简体   繁体   English

复制和移动成语?

[英]Copy & Move Idiom?

By using the Copy & Swap idiom we can easily implement copy assignment with strong exception safety: 通过使用Copy&Swap习语,我们可以轻松实现具有强大异常安全性的副本分配:

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

However this requires T to be Swappable . 然而,这需要T可交换的 Which a type automatically is if std::is_move_constructible_v<T> && std::is_move_assignable_v<T> == true thanks to std::swap . 如果std::is_move_constructible_v<T> && std::is_move_assignable_v<T> == true则自动生成哪种类型,这要归功于std::swap

My question is, is there any downside to using a "Copy & Move" idiom instead? 我的问题是,使用“复制和移动”成语有没有任何缺点? Like so: 像这样:

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

provided that you implement move-assignment for T because obviously you end up with infinite recursion otherwise. 只要你为T实现移动赋值,因为很明显你最终会得到无限递归。

This question is different from Should the Copy-and-Swap Idiom become the Copy-and-Move Idiom in C++11? 这个问题不同于复制和交换成语是否应该成为C ++ 11中的复制和移动成语? in that this question is more general and uses the move assignment operator instead of actually moving the members manually. 因为这个问题更通用,并且使用移动赋值运算符而不是手动移动成员。 Which avoids the issues with clean-up that predicted the answer in the linked thread. 这避免了在链接线程中预测答案的清理问题。

Correction to the question 更正问题

The way to implement Copy & Move has to be as @Raxvan pointed out: 实现Copy&Move的方法必须像@Raxvan所指出的那样:

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

but without the std::move as T(other) already is an rvalue and clang will emit a warning about pessimisation when using std::move here. 但没有std::move因为T(other)已经是一个rvalue而clang会在使用std::move时发出关于悲观的警告。

Summary 摘要

When a move assignment operator exists, the difference between Copy & Swap and Copy & Move is dependent on whether the user is using a swap method which has better exception safety than the move assignment. 当存在移动赋值运算符时,复制和交换以及复制和移动之间的差异取决于用户是否使用的swap方法具有比移动分配更好的异常安全性。 For the standard std::swap the exception safety is identical between Copy & Swap and Copy & Move. 对于标准的std::swap ,复制和交换以及复制和移动之间的异常安全性是相同的。 I believe that most of the time, it will be the case that swap and the move assignment will have the same exception safety (but not always). 我相信在大多数情况下, swap和移动分配将具有相同的异常安全性(但并非总是如此)。

Implementing Copy & Move has a risk where if the move assignment operator isn't present or has the wrong signature, the copy assignment operator will reduce to infinite recursion. 实现复制和移动存在风险,如果移动赋值运算符不存在或具有错误的签名,则复制赋值运算符将减少为无限递归。 However at least clang warns about this and by passing -Werror=infinite-recursion to the compiler this fear can be removed, which quite frankly is beyond me why that is not an error by default, but I digress. 然而,至少clang警告这一点,并通过将-Werror=infinite-recursion传递给编译器,这种恐惧可以被删除,这坦率地超出了我为什么这不是默认的错误,但我离题了。

Motivation 动机

I have done some testing and a lot of head scratching and here is what I have found out: 我已经做了一些测试和大量的头部刮擦,这是我发现的:

  1. If you have a move assignment operator, the "proper" way of doing Copy & Swap won't work due to the call to operator=(T) being ambiguous with operator=(T&&) . 如果你有一个移动赋值运算符,由于调用operator=(T)operator=(T&&)不明确,执行复制和交换的“正确”方式将不起作用。 As @Raxvan pointed out, you need to do the copy construction inside of the body of the copy assignment operator. 正如@Raxvan指出的那样,您需要在复制赋值运算符的主体内部进行复制构造。 This is considered inferior as it prevents the compiler from performing copy elision when the operator is called with an rvalue. 这被认为是次要的,因为当使用rvalue调用运算符时,它会阻止编译器执行复制省略。 However the cases where copy elision would have applied are handled by the move assignment now so that point is moot. 但是,复制省略将应用的情况现在由移动分配处理,因此这一点没有实际意义。

  2. We have to compare: 我们必须比较:

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

    to: 至:

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

    If the user isn't using a custom swap , then the templated std::swap(a,b) is used. 如果用户没有使用自定义swap ,则使用模板化的std::swap(a,b) Which essentially does this: 这基本上是这样的:

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

    Which means that the exception safety of Copy & Swap is the same exception safety as the weaker of move construction and move assignment. 这意味着Copy&Swap的异常安全性与移动构造和移动分配较弱的异常安全性相同。 If the user is using a custom swap, then of course the exception safety is dictated by that swap function. 如果用户正在使用自定义交换,那么异常安全性当然由该交换功能决定。

    In the Copy & Move, the exception safety is dictated entirely by the move assignment operator. 在复制和移动中,异常安全性完全由移动赋值运算符决定。

    I believe that looking at performance here is kind of moot as compiler optimizations will likely make there be no difference in most cases. 我相信在这里看性能有点没有实际意义,因为编译器优化可能会使大多数情况下没有差别。 But I'll remark on it anyway the copy and swap performs a copy construction, a move construction and two move assignments, compared to Copy & Move which does a copy construction and only one move assignment. 但是我会对它进行评论,无论如何复制和交换执行复制构造,移动构造和两个移动分配,与复制构造和仅一个移动分配的复制和移动相比。 Although I'm kind of expecting the compiler to crank out the same machine code in most cases, of course depending on T. 虽然我有点期望编译器在大多数情况下生成相同的机器代码,当然取决于T.

Addendum: The code I used 附录:我使用的代码

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

My question is, is there any downside to using a "Copy & Move" idiom instead? 我的问题是,使用“复制和移动”成语有没有任何缺点?

Yes, it you get stack overflow if you din't implement move assignment operator =(T&&) . 是的,如果您没有实现移动赋值operator =(T&&) ,则会出现堆栈溢出。 If you do want to implement that you get a compiler error ( 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);
    }

};

and if you do test a,b; a = b; 如果你test a,b; a = b; test a,b; a = b; you get the error: 你得到错误:

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

One way to solve this is to use a copy constructor: 解决此问题的一种方法是使用复制构造函数:

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

This will work, however if you don't implement move assignment you might not get an error (depending on compiler settings). 这将有效,但是如果您不实现移动分配,则可能不会出现错误(取决于编译器设置)。 Considering human error, it's possible that this case could happen and you end up stack overflow at runtime which is bad. 考虑到人为错误,这种情况可能会发生,并且在运行时最终会导致堆栈溢出,这很糟糕。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM