【问题标题】:Copy & Move Idiom?复制和移动成语?
【发布时间】:2017-09-08 04:02:40
【问题描述】:

通过使用Copy & Swap 习惯用法,我们可以轻松实现具有强大异常安全性的复制分配:

T& operator = (T other){
    using std::swap;
    swap(*this, other);
    return *this;
}

但是,这要求 TSwappable。如果std::is_move_constructible_v<T> && std::is_move_assignable_v<T> == true 感谢std::swap,则自动为哪种类型。

我的问题是,使用“复制和移动”习语有什么缺点吗?像这样:

T& operator = (T other){
    *this = std::move(other);
    return *this;
}

前提是您为 T 实现了移动分配,否则显然您最终会得到无限递归。

这个问题与Should the Copy-and-Swap Idiom become the Copy-and-Move Idiom in C++11? 的不同之处在于这个问题更笼统,并且使用移动赋值运算符而不是实际手动移动成员。这避免了在链接线程中预测答案的清理问题。

【问题讨论】:

  • 我还会在这个问题中添加另一种方法来做 operator = 与放置新:T& operator =(const T & other) { this->~T(); return * new(this) T(other); }
  • 如果你的移动构造函数抛出异常怎么办?这不会使复制和移动习语不安全吗?
  • @Raxvan 没有很强的异常安全性。
  • @EmilyL。好的,抱歉,我收回了我的重复标志。为防止其他人这样做,我建议您编辑您的问题并添加此内容。

标签: c++ c++11 move-semantics copy-and-swap


【解决方案1】:

更正问题

实现复制和移动的方式必须像@Raxvan 指出的那样:

T& operator=(const T& other){
    *this = T(other);
    return *this;
}

但没有std::move,因为T(other) 已经是一个右值,并且在此处使用std::move 时,clang 会发出有关悲观化的警告。

总结

当存在移动赋值运算符时,Copy & Swap 和 Copy & Move 之间的区别取决于用户是否使用swap 方法,该方法比移动赋值具有更好的异常安全性。对于标准std::swap,Copy & Swap 和 Copy & Move 之间的异常安全性是相同的。我相信大多数时候,swap 和 move 分配将具有相同的异常安全性(但并非总是如此)。

实现复制和移动存在风险,如果移动赋值运算符不存在或签名错误,则复制赋值运算符将减少为无限递归。然而,至少 clang 对此发出警告,通过将-Werror=infinite-recursion 传递给编译器,可以消除这种恐惧,坦率地说,这超出了我的理解,为什么默认情况下这不是错误,但我离题了。

动机

我已经做了一些测试和很多挠头,这就是我发现的:

    1234563正如@Raxvan 指出的那样,您需要在复制赋值运算符的主体内进行复制构造。这被认为是劣等的,因为它可以防止编译器在使用右值调用运算符时执行复制省略。但是,应用复制省略的情况现在由移动分配处理,因此这一点没有实际意义。
  1. 我们必须比较:

    T& operator=(const T& other){
        using std::swap;
        swap(*this, T(other));
        return *this;
    }
    

    到:

    T& operator=(const T& other){
        *this = T(other);
        return *this;
    }
    

    如果用户没有使用自定义swap,则使用模板化std::swap(a,b)。本质上是这样做的:

    template<typename T>
    void swap(T& a, T& b){
        T c(std::move(a));
        a = std::move(b);
        b = std::move(c);
    }
    

    这意味着 Copy & Swap 的异常安全性与 move 构造和 move assignment 中较弱的异常安全性相同。如果用户使用自定义交换,那么异常安全当然由交换函数决定。

    在复制和移动中,异常安全完全由移动赋值运算符决定。

    我相信在这里查看性能是没有实际意义的,因为编译器优化可能在大多数情况下没有区别。但无论如何我都会评论它,复制和交换执行复制构造、移动构造和两个移动分配,而复制和移动执行复制构造和仅一个移动分配。虽然我有点期待编译器在大多数情况下都能生成相同的机器代码,当然这取决于 T。

附录:我使用的代码

  class T {
  public:
    T() = default;
    T(const std::string& n) : name(n) {}
    T(const T& other) = default;

#if 0
    // Normal Copy & Swap.
    // 
    // Requires this to be Swappable and copy constructible. 
    // 
    // Strong exception safety if `std::is_nothrow_swappable_v<T> == true` or user provided
    // swap has strong exception safety. Note that if `std::is_nothrow_move_assignable` and
    // `std::is_nothrow_move_constructible` are both true, then `std::is_nothrow_swappable`
    // is also true but it does not hold that if either of the above are true that T is not
    // nothrow swappable as the user may have provided a specialized swap.
    //
    // Doesn't work in presence of a move assignment operator as T t1 = std::move(t2) becomes
    // ambiguous.
    T& operator=(T other) {
      using std::swap;
      swap(*this, other);
      return *this;
    }
#endif

#if 0
    // Copy & Swap in presence of copy-assignment.
    //
    // Requries this to be Swappable and copy constructible.
    //
    // Same exception safety as the normal Copy & Swap. 
    // 
    // Usually considered inferor to normal Copy & Swap as the compiler now cannot perform
    // copy elision when called with an rvalue. However in the presence of a move assignment
    // this is moot as any rvalue will bind to the move-assignment instead.
    T& operator=(const T& other) {
      using std::swap;

      swap(*this, T(other));
      return *this;
    }
#endif

#if 1
    // Copy & Move
    //
    // Requires move-assignment to be implemented and this to be copy constructible.
    //
    // Exception safety, same as move assignment operator.
    //
    // If move assignment is not implemented, the assignment to this in the body
    // will bind to this function and an infinite recursion will follow.
    T& operator=(const T& other) {
      // Clang emits the following if a user or default defined move operator is not present.
      // > "warning: all paths through this function will call itself [-Winfinite-recursion]"
      // I recommend  "-Werror=infinite-recursion" or "-Werror" compiler flags to turn this into an
      // error.

      // This assert will not protect against missing move-assignment operator.
      static_assert(std::is_move_assignable<T>::value, "Must be move assignable!");

      // Note that the following will cause clang to emit:
      // warning: moving a temporary object prevents copy elision [-Wpessimizing-move]

      // *this = std::move(T{other});

      // The move doesn't do anything anyway so write it like this;
      *this = T(other);
      return *this;
    }
#endif

#if 1
    T& operator=(T&& other) {
      // This will cause infinite loop if user defined swap is not defined or findable by ADL
      // as the templated std::swap will use move assignment.

      // using std::swap;
      // swap(*this, other);

      name = std::move(other.name);
      return *this;
    }
#endif

  private:
    std::string name;
  };

【讨论】:

    【解决方案2】:

    我的问题是,使用“复制和移动”习语有什么缺点吗?

    是的,如果你没有实现移动赋值operator =(T&amp;&amp;),你会得到堆栈溢出。 如果你确实想实现它,你会得到一个编译器错误 (example here):

    struct test
    {
        test() = default;
        test(const test &) = default;
    
        test & operator = (test t)
        {
            (*this) = std::move(t);
            return (*this);
        }
    
        test & operator = (test &&)
        {
            return (*this);
        }
    
    };
    

    如果你这样做 test a,b; a = b; 你会得到错误:

    error: ambiguous overload for 'operator=' (operand types are 'test' and 'std::remove_reference&lt;test&amp;&gt;::type {aka test}')

    解决这个问题的一种方法是使用复制构造函数:

    test & operator = (const test& t)
    {
        *this = std::move(test(t));
        return *this;
    }
    

    这会起作用,但是如果您不实现移动分配,您可能不会收到错误(取决于编译器设置)。考虑到人为错误,这种情况可能会发生,并且您最终会在运行时导致堆栈溢出,这很糟糕。

    【讨论】:

    • 但是你当然会编写单元测试来确保它不会发生,对吧? ;)
    • 使用这样的复制构造函数会阻止编译器执行复制省略:/此外,如果上面的操作符 = 不明确,那么复制和交换也不行吗?
    • 但是,只有当它是一个右值并且无论如何都由移动分配处理时,复制省略才会应用......
    猜你喜欢
    • 2013-10-17
    • 2013-11-05
    • 1970-01-01
    • 1970-01-01
    • 2015-10-28
    • 2011-06-24
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多