2017-04-12 64 views
21

通过使用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,哪种类型会自动变为。

我的问题是,使用“Copy & Move”成语会有什么不足吗?像这样:

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?不同,因为这个问题更一般,使用移动赋值操作符而不是实际上手动移动成员。这避免了预测链接线程中的答案的清理问题。

+2

的代码我也将增加这个问题的另一种方式来做'operator ='放置new:'T&operator =(const T&other){this->〜T();返回* new(this)T(other); }' – Raxvan

+0

的可能的复制[应复制和交换成语成为复制和移动成语在C++ 11?](http://stackoverflow.com/questions/24014130/should-the-copy-and -swap-idiom-become-the-copy-and-move-idiom-in-c11) –

+0

如果你的移动构造函数抛出异常怎么办?这不会使复制和移动成语不安全? –

回答

3

My question is, is there any downside to using a "Copy & Move" idiom instead?

是的,你会得到堆栈溢出,如果你din't实现移动分配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; 
} 

但是,如果您不执行移动赋值你可能不会得到一个错误(取决于编译器设置)。考虑到人为错误,可能会发生这种情况,并且在运行时会导致堆栈溢出,这很糟糕。

+0

但是你当然写了单元测试以确保它不会发生,对吧? ;) –

+0

使用这样的拷贝构造函数将阻止编译器执行copy-elision:/如果operator =在上面不明确,那么复制和交换将无法正常工作? –

+0

但是,然后复制elision只会应用,如果它是一个r值,并且由移动任务处理...... –

6

更正问题

的方式来实现复制&此举是为@Raxvan指出:

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

但没有std::move作为T(other)已经是一个右值和铛会散发出在这里使用std::move时,警告关于悲观情绪。

摘要

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

实现复制&移动有一个风险,其中如果移动赋值操作符不存在或有错误的签名,拷贝赋值运算符将减少到无限递归。但是至少铛发出警告这一点,并通过传递-Werror=infinite-recursion编译器这种担心可以去掉,这很坦率地说我是无法理解为什么这是不是默认的错误,但我离题。

动机

我已经做了一些测试和大量的头划伤和这里的是我发现:

  1. 如果你有一招赋值操作符的“正确”的方式做复制&交换不会因调用工作operator=(T)是暧昧与operator=(T&&)。正如@Raxvan指出的那样,您需要在复制赋值运算符的主体内部执行复制构造。这被认为是较差的,因为它妨碍编译器在用右值调用运算符时执行复制elision。然而,复制删除将应用的情况现在由移动分配来处理,因此该点是没有意义的。

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

    这意味着复制&交换的异常安全性是一样的异常安全的举动建设较弱,移动分配。如果用户使用自定义交换,那么异常安全性当然是由交换功能决定的。

    在复制&移动,异常安全的举动赋值运算符完全决定。

    我相信,在性能看这里是一种毫无意义的编译器优化将可能使有在大多数情况下没有什么区别。但无论如何,无论如何,我会对它进行评论,副本和掉期执行副本构建,移动构建和两个移动分配,而复制&移动副本构建和只有一个移动任务。虽然我是那种希望编译器根据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; 
    };