在运算符重载中消除临时变量

4

注意:正如sellibitze所指出的,我对rvalue引用不是最新的了解,因此我提出的方法存在错误,请阅读他的答案以了解哪些错误。

昨天我在阅读Linus的抱怨之一,其中有一个抱怨是针对运算符重载的。

似乎抱怨的原因是如果您有一个类型为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; }

您认为这组运算符是否足够?在您看来,它是否具有普适性?

*:此实现假设可交换性,对于臭名昭著的字符串不起作用。


问题中没有说明:如果这个集合足够的话,意味着我们需要为每个运算符添加3个新的重载...除了使用Boost.Operator之外,我看不到其他自动生成它们的方法。从我的角度来看,这意味着对于std::string,由于std::stringchar const*的混合,需要有8个operator+ - Matthieu M.
那太糟糕了。他们还称之为“解决方案”。噗。 - Johannes Schaub - litb
@litb:我同意你的感觉……特别是当我想到“-”、“*”、“/”和所有其他类似的运算符时。数字类将有更多的理由从boost::addable等类中继承,我想。 - Matthieu M.
1个回答

4
如果您正在考虑一个自定义的、可移动的字符串类,利用每种参数值类别的正确方法是:
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。通过值返回对象是完全可以的。它将创建临时对象,但这些对象是rvalues,所以我们可以窃取它们的资源使其非常有效。

其次,您的代码也不应该编译,原因与以下代码不编译相同:

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+通过值获取其左侧,因此会导致许多副本。上面的四个重载函数也完美处理了这个示例。

我已经有一段时间没有读林纳斯的旧抱怨了。如果他在抱怨std::string的不必要复制,那么在C++0x中,这种抱怨已经不再有效,但在C ++03之前几乎无效。您可以有效地连接许多字符串:

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

但在C++0x中,您还可以使用operator+和std::move。这也非常高效。

我实际上查看了Git源代码及其字符串管理(strbuf.h)。它看起来经过深思熟虑。除了detach/attach功能外,您可以使用启用移动的std::string获取相同的内容,明显的优势是资源由类自身自动管理,而不是需要用户在正确的时间调用正确的函数(strbuf_init,strbuf_release)。


感谢你发现了这个错误(返回 &&)。我还没有开始使用右值引用,所以在这方面也不是最新的。但这意味着需要四个重载函数才能正确处理它……我的… - Matthieu M.

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接