复制和移动成语?

Emi*_* L. 22 c++ move-semantics copy-and-swap c++11

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

T& operator = (T other){
    using std::swap;
    swap(*this, other);
    return *this;
}
Run Code Online (Sandbox Code Playgroud)

但是这需要T可交换的.如果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;
}
Run Code Online (Sandbox Code Playgroud)

只要你实现了move-assignment,T因为很明显你最终会得到无限递归.

这个问题不同于复制和交换成语是否应该成为C++ 11中的复制和移动成语?因为这个问题更通用,并且使用移动赋值运算符而不是手动移动成员.这避免了在链接线程中预测答案的清理问题.

Emi*_* L. 8

更正问题

实现Copy&Move的方法必须像@Raxvan所指出的那样:

T& operator=(const T& other){
    *this = T(other);
    return *this;
}
Run Code Online (Sandbox Code Playgroud)

但没有std::moveT(other)已经是当使用一个右值和铛会发出约pessimisation警告std::move这里.

摘要

当存在移动赋值运算符时,复制和交换以及复制和移动之间的差异取决于用户是否使用的swap方法具有比移动分配更好的异常安全性.对于标准std::swap,复制和交换以及复制和移动之间的异常安全性是相同的.我相信在大多数情况下,情况都是如此,swap并且移动任务将具有相同的异常安全性(但并非总是如此).

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

动机

我已经做了一些测试和大量的头部刮擦,这是我发现的:

  1. 如果你有一个移动赋值运算符,由于调用operator=(T)不明确,执行复制和交换的"正确"方式将不起作用operator=(T&&).正如@Raxvan指出的那样,您需要在复制赋值运算符的主体内部进行复制构造.这被认为是次要的,因为当使用rvalue调用运算符时,它会阻止编译器执行复制省略.但是,复制省略将应用的情况现在由移动分配处理,因此这一点没有实际意义.

  2. 我们必须比较:

    T& operator=(const T& other){
        using std::swap;
        swap(*this, T(other));
        return *this;
    }
    
    Run Code Online (Sandbox Code Playgroud)

    至:

    T& operator=(const T& other){
        *this = T(other);
        return *this;
    }
    
    Run Code Online (Sandbox Code Playgroud)

    如果用户未使用自定义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);
    }
    
    Run Code Online (Sandbox Code Playgroud)

    这意味着Copy&Swap的异常安全性与移动构造和移动分配较弱的异常安全性相同.如果用户正在使用自定义交换,那么异常安全性当然由该交换功能决定.

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

    我相信在这里看性能有点没有实际意义,因为编译器优化可能会使大多数情况下没有差别.但是我会对它进行评论,无论如何复制和交换执行复制构造,移动构造和两个移动分配,与复制构造和仅一个移动分配的复制和移动相比.虽然我有点期望编译器在大多数情况下生成相同的机器代码,当然取决于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;
  };
Run Code Online (Sandbox Code Playgroud)


Rax*_*van 5

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

是的,如果您不实现移动分配,就会出现堆栈溢出operator =(T&&)。如果你确实想实现它,你会得到一个编译器错误(示例在这里):

struct test
{
    test() = default;
    test(const test &) = default;

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

    test & operator = (test &&)
    {
        return (*this);
    }

};
Run Code Online (Sandbox Code Playgroud)

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

error: ambiguous overload for 'operator=' (operand types are 'test' and 'std::remove_reference<test&>::type {aka test}')

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

test & operator = (const test& t)
{
    *this = std::move(test(t));
    return *this;
}
Run Code Online (Sandbox Code Playgroud)

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