复制&移动成语?

时间:2017-04-12 11:35:51

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

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

byte[] byteArray = rs.getBytes("columnname");  

Bitmap bm = BitmapFactory.decodeByteArray(byteArray, 0 ,byteArray.length);

但是,这需要T& operator = (T other){ using std::swap; swap(*this, other); return *this; } Swappable。感谢std::swap,如果T,则会自动输入哪种类型。

我的问题是,使用"复制和放大是否有任何不利之处?移动"成语而不是?像这样:

std::is_move_constructible_v<T> && std::is_move_assignable_v<T> == true

前提是您为T& operator = (T other){ *this = std::move(other); return *this; } 实现了移动分配,因为显然您最终会得到无限递归。

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

2 个答案:

答案 0 :(得分:8)

对问题的更正

实施Copy&amp; amp;移动必须像@Raxvan指出的那样:

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

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

摘要

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

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

动机

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

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

  2. 我们必须比较:

    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&amp; amp;的异常安全性。交换是与移动构造和移动分配较弱的异常安全相同。如果用户使用自定义交换,那么异常安全性当然由该交换功能决定。

    在Copy&amp;移动,异常安全完全由移动赋值运算符决定。

    我认为在这里看性能有点没有实际意义,因为编译器优化可能会使大多数情况下没有差别。但是我会说它,无论如何复制和交换执行复制构造,移动构造和两个移动分配,与Copy&amp;移动,它执行复制构造,只有一个移动分配。虽然我有点期望编译器在大多数情况下生成相同的机器代码,当然取决于T。

  3. 附录:我使用的代码

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

答案 1 :(得分:3)

  

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

是的,如果您不实施移动分配operator =(T&&),则会出现堆栈溢出。 如果您确实希望实现编译器错误(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<test&>::type {aka test}')

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

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

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

相关问题