X值可能是rvalue,但这并不意味着它们是“临时”的。临时对象的生存期延长来自于它们是“临时”的这个事实,而不是它们的值类别。
我特意不知道运算符处理的顺序(这样,我就迫使自己编写代码,要么使用显式括号,要么根本不关心顺序)。你的特定的adder
示例代码,在此重复一下,确实关心:
template <class T>
struct addable
{
friend T operator +( const T& lhs, const T& rhs )
{
return std::move(T(lhs) += rhs);
}
friend T operator +( const T& lhs, T&& rhs )
{
return std::move(T(lhs) += std::move(rhs));
}
friend T&& operator +( T&& lhs, const T& rhs )
{
return std::move(lhs += rhs);
}
friend T&& operator +( T&& lhs, T&& rhs )
{
return std::move(lhs += std::move(rhs));
}
};
如果
+
运算符是从右往左执行的,那么
t1 + t2 + t3
会被计算为
t1 + (t2 + t3)
。调用
t2 + t3
将调用第一个重载,因此产生一个临时变量,得到
t1 + temp
。由于临时变量更倾向于绑定到一个右值引用上,因此该表达式将调用第二个重载,其也将返回一个临时变量。
然而,如果
+
运算符从左往右执行,那么你会得到
(t1 + t2) + t3
。这给我们
temp + t1
,这会导致问题。它将调用第三个重载。该函数的
lhs
参数是一个
T&&
,是对临时变量的引用。你返回相同的引用。这意味着你返回了对临时变量的引用。但是C++不知道这一点;它只知道你正在返回对某个东西的引用。
然而,这个“东西”在最后一个表达式(分配给新变量,可以是值类型或引用类型)被评估之后就要被销毁了。记住:C++不知道这个函数将返回对其第一个参数的引用。因此它无法知道用于函数操作数的临时变量的寿命需要扩展到存储返回引用的寿命。
顺便说一句,这就是为什么带有
auto
和相关内容的表达式树可能很危险。因为创建的内部临时变量无法由新临时变量或存储在各种对象中的引用保留。C++没有办法做到这一点。
所以谁是正确的取决于运算符解析的顺序。然而,我更喜欢我的解决方案:不要依赖语言的这些角落,只需绕过它们。停止从这些重载中返回
T&&
,而是将值移动到临时变量中。这样,它保证能够正确工作,你也不必不断地检查标准以确保你的代码能够正常工作。
此外,顺便说一下,我认为运算符+实际上修改其中一个参数有点粗鲁。
然而,如果您坚持要知道谁是正确的,那么GCC是正确的。来自第5.7节,第1页:
加法和减法运算符按从左到右分组。
所以,它不应该起作用。
注意:Visual Studio允许
T &r2 = t1 + t2 + t3;
编译为(非常让人恼火的)语言扩展。您应该已经得到了警告。