关于此答案的重要更新/警告!
实际上有一个令人信服的例子,它在合理的真实世界代码中默默地创建了一个悬空引用,如下所示。请使用其他答案的技术来避免此问题,即使以创建一些额外的临时对象为代价。我将保留此答案的其余部分,以供将来参考。
可交换情况的正确重载是:
T operator+( const T& lhs, const T& rhs )
{
T nrv( lhs );
nrv += rhs;
return nrv;
}
T&& operator+( T&& lhs, const T& rhs )
{
lhs += rhs;
return std::move( lhs );
}
T&& operator+( const T& lhs, T&& rhs )
{
rhs += lhs;
return std::move( rhs );
}
T&& operator+( T&& lhs, T&& rhs )
{
lhs += std::move( rhs );
return std::move( lhs );
}
为什么会这样,它是如何工作的?首先,请注意,如果您将右值引用作为参数,您可以修改并返回它。它来自的表达式需要保证右值在完整表达式结束之前不会被破坏,包括operator+
. 这也意味着operator+
可以简单地返回右值引用,因为调用者需要在表达式被完全评估并且临时变量(ravlues)被破坏之前使用operator+
(它是同一表达式的一部分)的结果。
第二个重要的观察是,这如何节省更多的临时和移动操作。考虑以下表达式:
T a, b, c, d; // initialized somehow...
T r = a + b + c + d;
与上述内容,它相当于:
T t( a ); // T operator+( const T& lhs, const T& rhs );
t += b; // ...part of the above...
t += c; // T&& operator+( T&& lhs, const T& rhs );
t += d; // T&& operator+( T&& lhs, const T& rhs );
T r( std::move( t ) ); // T&& was returned from the last operator+
将此与其他方法发生的情况进行比较:
T t1( a ); // T operator+( T lhs, const T& rhs );
t1 += b; // ...part of the above...
T t2( std::move( t1 ) ); // t1 is an rvalue, so it is moved
t2 += c;
T t3( std::move( t2 ) );
t3 += d;
T r( std::move( t3 );
这意味着您仍然有三个临时对象,尽管它们被移动而不是复制,但上述方法在完全避免临时对象方面效率更高。
有关完整的库,包括对 的支持noexcept
,请参阅df.operators。在那里,您还将找到非交换案例和混合类型操作的版本。
这是一个完整的测试程序来测试它:
#include <iostream>
#include <utility>
struct A
{
A() { std::cout << "A::A()" << std::endl; }
A( const A& ) { std::cout << "A::A(const A&)" << std::endl; }
A( A&& ) { std::cout << "A::A(A&&)" << std::endl; }
~A() { std::cout << "A::~A()" << std::endl; }
A& operator+=( const A& ) { std::cout << "+=" << std::endl; return *this; }
};
// #define BY_VALUE
#ifdef BY_VALUE
A operator+( A lhs, const A& rhs )
{
lhs += rhs;
return lhs;
}
#else
A operator+( const A& lhs, const A& rhs )
{
A nrv( lhs );
nrv += rhs;
return nrv;
}
A&& operator+( A&& lhs, const A& rhs )
{
lhs += rhs;
return std::move( lhs );
}
#endif
int main()
{
A a, b, c, d;
A r = a + b + c + d;
}