【问题标题】:Eliminating temporaries in operator overloading消除运算符重载中的临时变量
【发布时间】:2010-09-25 07:04:42
【问题描述】:

注意: 正如 sellibitze 所指出的,我不是最新的右值引用,因此我提出的方法包含错误,请阅读他的回答以了解哪个。

我昨天正在阅读Linus' rant 的一篇文章,并且(某处)有人对运算符重载进行咆哮。

抱怨似乎是,如果你有一个S 类型的对象,那么:

S a = b + c + d + e;

可能涉及很多临时工。

在 C++03 中,我们有复制省略来防止这种情况:

S a = ((b + c) + d) + e;

我希望最后一个... + e 被优化,但我想知道有多少临时创建了用户定义的operator+

线程中有人建议使用表达式模板来处理这个问题。

现在,这个帖子可以追溯到 2007 年,但现在当我们想到消除临时性时,我们会想到 Move

所以我在考虑我们应该编写的一组重载运算符不是为了消除临时性,而是为了限制它们的构造成本(窃取资源)。

S&& operator+(S&& lhs, S const& rhs) { return lhs += rhs; }
S&& operator+(S const& lhs, S&& rhs) { return rhs += lhs; } // *
S&& operator+(S&& lhs, S&& rhs) { return lhs += rhs; }

这组操作符看起来够吗?这是否可以概括(在您看来)?

*:这个实现假设可交换性,它不适用于臭名昭著的string

【问题讨论】:

  • 问题中没有说明:如果这个集合足够,这意味着我们有 3 个新的重载要为每个运算符添加......除了使用 Boost.Operator 我看不到自动生成它们的方法。从头到尾,这意味着 8 operator+ for std::string 因为 std::stringchar const* 混合......
  • 呃,这太可怕了。他们称之为“解决方案”。噗
  • @litb:我同意你的感觉……尤其是当我想到-*/ 和所有其他类似的运算符时。我猜数字类将有更多的理由从boost::addable 和 co 继承。

标签: c++ operator-overloading c++11


【解决方案1】:

如果您正在考虑自定义、启用移动的字符串类,那么利用参数值类别的每个组合的正确方法是:

S operator+(S const& lhs, S const& rhs);
S operator+(S     && lhs, S const& rhs);
S operator+(S const& lhs, S     && rhs);
S operator+(S     && lhs, S     && rhs);

函数返回 prvalue 而不是 xvalue。返回 xvalues 通常是一件非常危险的事情——std::move 和 std::forward 是明显的例外。如果你要返回一个右值引用,你会破坏这样的代码:

for (char c : my_string + other_string) {
   //...
}

这个循环的行为(根据 N3092 中的 6.5.4/1)就像代码是:

auto&& range = my_string + other_string;

这反过来会导致悬空引用。临时对象的生命周期没有延长,因为您的 operator+ 不返回 prvalue。按值返回对象非常好。它会创建临时对象,但这些对象是右值,因此我们可以窃取它们的资源以使其非常有效。

其次,您的代码也不应该因为无法编译的原因而编译:

int&& foo(int&& x) { return x; }

函数体内 x 是一个左值,您不能用左值表达式初始化“返回值”(在本例中为右值引用)。所以,你需要一个明确的演员表。

第三,您缺少 const&+const& 重载。如果您的两个参数都是左值,编译器将不会在您的情况下找到可用的 operator+。

如果你不想要这么多的重载,你也可以这样写:

S operator+(S value, S const& x)
{
   value += x;
   return value;
}

我故意没有写return value+=x;,因为这个运算符可能返回一个左值引用,这会导致返回值的复制构造。用我写的两行,返回值将由 value 构造。

S x = a + b + c + d;

至少这种情况非常有效,因为即使编译器无法删除副本,也不会涉及不必要的复制——这要归功于支持移动的字符串类。实际上,使用像 std::string 这样的类,你可以利用它的快速交换成员函数并使其在 C++03 中也有效,前提是你有一个相当聪明的编译器(如 GCC):

S operator+(S value, S const& x) // pass-by-value to exploit copy elisions
{
   S result;
   result.swap(value);
   result += x;
   return result; // NRVO applicable
}

请参阅 David Abraham 的文章 Want Speed? Pass by Value 但是这些简单的运算符不会那么有效:

S x = a + (b + (c + d));

这里操作符的左边总是一个左值。由于 operator+ 按值取其左侧,这会导致许多副本。上面的四个重载也完美地处理了这个例子。

我已经有一段时间没有读到 Linus 的旧话了。如果他抱怨关于 std::string 的不必要的副本,那么这个抱怨在 C++0x 中不再有效,但以前几乎无效。您可以在 C++03 中高效地连接多个字符串:

S result = a;
result += b;
result += c;
result += d;

但是在 C++0x 中你也可以使用 operator+ 和 std::move。这也将非常有效。

我实际上查看了 Git 源代码及其字符串管理 (strbuf.h)。它看起来经过深思熟虑。除了分离/附加功能之外,您还可以通过启用移动的 std::string 获得相同的东西,其明显优势在于它由类本身自动管理的资源,而不是需要记住调用正确函数的用户正确的时间(strbuf_init,strbuf_release)。

【讨论】:

  • 感谢您发现错误(返回&&)我还没有时间使用右值引用,所以我不是最新的。尽管如此,这意味着要正确处理这个问题需要 4 个重载......我的......
猜你喜欢
  • 2012-10-10
  • 2023-03-11
  • 1970-01-01
  • 2010-10-19
  • 2016-03-17
  • 1970-01-01
  • 2017-06-30
  • 1970-01-01
相关资源
最近更新 更多