Copy & Move Idiom?
Asked Answered
F

2

28

By using the Copy & Swap idiom we can easily implement copy assignment with strong exception safety:

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

However this requires T to be Swappable. Which a type automatically is if std::is_move_constructible_v<T> && std::is_move_assignable_v<T> == true thanks to 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.

This question is different from Should the Copy-and-Swap Idiom become the Copy-and-Move Idiom in 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.

Ferdy answered 12/4, 2017 at 11:35 Comment(10)
I would also add to this question another way to do operator = with placement new : T& operator =(const T & other) { this->~T(); return * new(this) T(other); }Reeher
Possible duplicate of Should the Copy-and-Swap Idiom become the Copy-and-Move Idiom in C++11?Ionosphere
What if your move constructor throws an exception? Won't that make copy and move idiom unsafe?Kassiekassity
@Reeher that doesn't have strong exception safety.Ferdy
@FabioTurati This question is different as the the linked 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.Ferdy
@AbdusSalamKhazi If move constructor throws, then default swap will also throw as it uses move constructor. A specialized swap might avoid a throw which is a good point.Ferdy
@Reeher Your method is not exception safe and it invalidates all existing references to T since its lifetime ends at ~T().Bravar
@EmilyL. Ok, sorry, I've retracted my duplicate flag. To prevent others from doing it, I would recommend to edit your question and add this.Ionosphere
@Maxim Egorushkin and @Emily L. does not compile if you implement move assignment T & operator (T&& other)Reeher
@HowardHinnant It's well known that copy & swap is slower than a hand written copy assignment. And the reason is that vector can reuse the capacity if you don't allocate a new one every time. But that's not the point of the question.Ferdy
F
14

Correction to the question

The way to implement Copy & Move has to be as @Raxvan pointed out:

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.

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. For the standard std::swap the exception safety is identical between Copy & Swap and Copy & Move. 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).

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.

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&&). As @Raxvan pointed out, you need to do the copy construction inside of the body of the copy assignment operator. This is considered inferior as it prevents the compiler from performing copy elision when the operator is called with an 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. 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. 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.

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;
  };
Ferdy answered 12/4, 2017 at 14:46 Comment(0)
R
5

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 assignmentoperator =(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; 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.

Reeher answered 12/4, 2017 at 12:15 Comment(3)
But of course you do write unit tests to make sure it doesn't happen, right? ;)Ferdy
Using the copy constructor like that will prevent the compiler from performing copy-elision though :/ Also if operator = is ambiguous in the above, then copy and swap will not work either right?Ferdy
But then again copy elision would only have applied if it was an r-value and that is handled by the move assignment anyway...Ferdy

© 2022 - 2024 — McMap. All rights reserved.