【问题标题】:Move semantics and operator overloading移动语义和运算符重载
【发布时间】:2013-04-14 17:25:59
【问题描述】:

这与 Matthieu M. 提供的 this answer 有关如何使用带有 + 运算符重载的移动语义(通常是不直接重新分配回左侧参数的运算符)。

他建议实现三个不同的重载:

inline T operator+(T left, T const& right) { left += right; return left; }
inline T operator+(T const& left, T right) { right += left; return right; } // commutative
inline T operator+(T left, T&& right) { left += right; return left; } // disambiguation

数字 1 和 3 是有道理的,但我不明白 2 的目的是什么。该评论建议进行交换处理,但似乎 1 和 2 将是互斥的(即在模棱两可的情况下实现这两个结果)

例如,所有 3 个都已实现:

T a, b, c;
c = a + b;

编译器输出:

1> 错误 C2593: 'operator +' 不明确 1> 可以是 'T operator +(const T &,T)' 1> 或 'T 运算符 +(T,const T &)' 1> 在尝试匹配参数列表 '(T, T)' 时

删除 1 或 2,程序按预期运行。由于 1 是一般情况,而 2 只能与可交换运算符一起正常工作,所以我不明白为什么会使用 2。我有什么遗漏吗?

【问题讨论】:

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


    【解决方案1】:

    我认为您没有遗漏任何内容-您问题中的代码确实很麻烦。他的回答的前面部分是有道理的,但是在“四个期望的情况”和实际示例之间丢失了一些东西。

    这可能会更好:

    inline T operator+(T left, T const& right) { left += right; return left; }
    inline T operator+(const T& left, T&& right) { right += left; return right; }
    

    这实现了规则:制作 LHS 的副本(最好通过移动构造),除非 RHS 无论如何都会过期,在这种情况下修改它。

    对于非交换运算符,省略第二个重载,或者提供不委托给复合赋值的实现。

    如果您的类中嵌入了重量级资源(因此无法有效地移动),您将希望避免按值传递。丹尼尔在他的回答中提出了一些很好的观点。但不要像他建议的那样返回T&&,因为这是一个悬空引用。

    【讨论】:

    • 我是否正确地说如果 op(left,right) 不是可交换的,则可以通过忽略第二个重载(const T& left, T&& right)来完成正确的处理?
    • @helloworld922:是的,没错。在这种情况下,右侧无法就地修改,因此第二次重载没有速度优势。
    • 如果两个都是右值,哪个会被调用?我们可以再添加一个inline T operator+(T&&,T&&),然后根据哪个资源更大来选择复用哪个?
    • @balki:就我而言,第二个重载被调用。是的,您的建议会奏效——是否值得取决于您的数据类型的具体情况。
    • 不应该是“inline T operator+(const T& left, T&& right) { right += left; return std::move(right); }”吗?因为函数里面的right是一个左值,所以return会做一个right的拷贝。
    【解决方案2】:

    关于此答案的重要更新/警告!

    实际上一个convincing example,它在合理的真实世界代码中默默地创建了一个悬空引用,如下所示。请使用 other 答案的技巧来避免这个问题,即使代价是创建一些额外的临时对象。我将保留此答案的其余部分,以供将来参考。


    可交换情况的正确重载是:

    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+ 可以简单地返回右值引用,因为调用者需要使用operator+ 的结果(它是同一个表达式的一部分),然后表达式被完全评估并且临时(ravlues) 被破坏。

    第二个重要的观察是,这如何节省更多的临时和移动操作。考虑以下表达式:

    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;
    }
    

    【讨论】:

    • 你真的不需要那么多版本。请参阅cpp-next.com/archive/2009/08/want-speed-pass-by-value 我的答案中的第一个版本看起来可能涉及额外的移动操作,但这可以并且在内联期间被编译器忽略。
    • @BenVoigt 不,它没有。 试试看!(我很讨厌听到这篇文章,每个人都认为按值传递可以解决所有问题。事实并非如此。再次:试试看。)
    • @helloworld922 请不要这样引用我的话。我说的是“在避免临时工方面效率更高”,而不是“在许多情况下效率更高”。在某些情况下,您可能看不到差异,因为稍后会进行其他优化和/或因为您的用例根本无法从中受益,但重要的是我的版本从未效率较低,所以它从不痛。此外,使用库意味着您甚至不需要关心。集中精力让operator+= 正确,剩下的交给df.operators
    • @BenVoigt 你读过 Dave 文章的“Reality Bites”部分吗?即使编译器将来能够进行这种优化(特别是对于优化确实很重要的非平凡情况),这也意味着优化过程的压力很大。我的技术今天有效(在 GCC 和 Clang 上测试)。
    • @DanielFrey:不能以与 every 内置类型和 every 标准库类型相同的方式使用的运算符重载是坏了。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2011-11-19
    • 2020-12-28
    • 1970-01-01
    • 2021-08-18
    • 2016-02-19
    • 2012-11-26
    相关资源
    最近更新 更多