Should the Copy-And-Swap Idiom Become the Copy-And-Move Idiom in C++11

Should the Copy-and-Swap Idiom become the Copy-and-Move Idiom in C++11?

It's been a long time since I asked this question, and I've known the answer for a while now, but I've put off writing the answer for it. Here it is.

The answer is no. The Copy-and-swap idiom should not become the Copy-and-move idiom.

An important part of Copy-and-swap (which is also Move-construct-and-swap) is a way to implement assignment operators with safe cleanup. The old data is swapped into a copy-constructed or move-constructed temporary. When the operation is done, the temporary is deleted, and its destructor is called.

The swap behaviour is there to be able to reuse the destructor, so you don't have to write any cleanup code in your assignment operators.

If there's no cleanup behaviour to be done and only assignment, then you should be able to declare the assignment operators as default and copy-and-swap isn't needed.

The move constructor itself usually doesn't require any clean-up behaviour, since it's a new object. The general simple approach is to make the move constructor invoke the default constructor, and then swap all the members with the move-from object. The moved-from object will then be like a bland default-constructed object.

However, in this question's observer pattern example, that's actually an exception where you have to do extra cleanup work because references to the old object need to be changed. In general, I would recommend making your observers and observables, and other design constructs based around references, unmovable whenever possible.

What is the copy-and-swap idiom?

Overview

Why do we need the copy-and-swap idiom?

Any class that manages a resource (a wrapper, like a smart pointer) needs to implement The Big Three. While the goals and implementation of the copy-constructor and destructor are straightforward, the copy-assignment operator is arguably the most nuanced and difficult. How should it be done? What pitfalls need to be avoided?

The copy-and-swap idiom is the solution, and elegantly assists the assignment operator in achieving two things: avoiding code duplication, and providing a strong exception guarantee.

How does it work?

Conceptually, it works by using the copy-constructor's functionality to create a local copy of the data, then takes the copied data with a swap function, swapping the old data with the new data. The temporary copy then destructs, taking the old data with it. We are left with a copy of the new data.

In order to use the copy-and-swap idiom, we need three things: a working copy-constructor, a working destructor (both are the basis of any wrapper, so should be complete anyway), and a swap function.

A swap function is a non-throwing function that swaps two objects of a class, member for member. We might be tempted to use std::swap instead of providing our own, but this would be impossible; std::swap uses the copy-constructor and copy-assignment operator within its implementation, and we'd ultimately be trying to define the assignment operator in terms of itself!

(Not only that, but unqualified calls to swap will use our custom swap operator, skipping over the unnecessary construction and destruction of our class that std::swap would entail.)



An in-depth explanation

The goal

Let's consider a concrete case. We want to manage, in an otherwise useless class, a dynamic array. We start with a working constructor, copy-constructor, and destructor:

#include <algorithm> // std::copy
#include <cstddef> // std::size_t

class dumb_array
{
public:
// (default) constructor
dumb_array(std::size_t size = 0)
: mSize(size),
mArray(mSize ? new int[mSize]() : nullptr)
{
}

// copy-constructor
dumb_array(const dumb_array& other)
: mSize(other.mSize),
mArray(mSize ? new int[mSize] : nullptr)
{
// note that this is non-throwing, because of the data
// types being used; more attention to detail with regards
// to exceptions must be given in a more general case, however
std::copy(other.mArray, other.mArray + mSize, mArray);
}

// destructor
~dumb_array()
{
delete [] mArray;
}

private:
std::size_t mSize;
int* mArray;
};

This class almost manages the array successfully, but it needs operator= to work correctly.

A failed solution

Here's how a naive implementation might look:

// the hard part
dumb_array& operator=(const dumb_array& other)
{
if (this != &other) // (1)
{
// get rid of the old data...
delete [] mArray; // (2)
mArray = nullptr; // (2) *(see footnote for rationale)

// ...and put in the new
mSize = other.mSize; // (3)
mArray = mSize ? new int[mSize] : nullptr; // (3)
std::copy(other.mArray, other.mArray + mSize, mArray); // (3)
}

return *this;
}

And we say we're finished; this now manages an array, without leaks. However, it suffers from three problems, marked sequentially in the code as (n).

  1. The first is the self-assignment test.

    This check serves two purposes: it's an easy way to prevent us from running needless code on self-assignment, and it protects us from subtle bugs (such as deleting the array only to try and copy it). But in all other cases it merely serves to slow the program down, and act as noise in the code; self-assignment rarely occurs, so most of the time this check is a waste.

    It would be better if the operator could work properly without it.

  2. The second is that it only provides a basic exception guarantee. If new int[mSize] fails, *this will have been modified. (Namely, the size is wrong and the data is gone!)

    For a strong exception guarantee, it would need to be something akin to:

     dumb_array& operator=(const dumb_array& other)
    {
    if (this != &other) // (1)
    {
    // get the new data ready before we replace the old
    std::size_t newSize = other.mSize;
    int* newArray = newSize ? new int[newSize]() : nullptr; // (3)
    std::copy(other.mArray, other.mArray + newSize, newArray); // (3)

    // replace the old data (all are non-throwing)
    delete [] mArray;
    mSize = newSize;
    mArray = newArray;
    }

    return *this;
    }
  3. The code has expanded! Which leads us to the third problem: code duplication.

Our assignment operator effectively duplicates all the code we've already written elsewhere, and that's a terrible thing.

In our case, the core of it is only two lines (the allocation and the copy), but with more complex resources this code bloat can be quite a hassle. We should strive to never repeat ourselves.

(One might wonder: if this much code is needed to manage one resource correctly, what if my class manages more than one?

While this may seem to be a valid concern, and indeed it requires non-trivial try/catch clauses, this is a non-issue.

That's because a class should manage one resource only!)

A successful solution

As mentioned, the copy-and-swap idiom will fix all these issues. But right now, we have all the requirements except one: a swap function. While The Rule of Three successfully entails the existence of our copy-constructor, assignment operator, and destructor, it should really be called "The Big Three and A Half": any time your class manages a resource it also makes sense to provide a swap function.

We need to add swap functionality to our class, and we do that as follows†:

class dumb_array
{
public:
// ...

friend void swap(dumb_array& first, dumb_array& second) // nothrow
{
// enable ADL (not necessary in our case, but good practice)
using std::swap;

// by swapping the members of two objects,
// the two objects are effectively swapped
swap(first.mSize, second.mSize);
swap(first.mArray, second.mArray);
}

// ...
};

(Here is the explanation why public friend swap.) Now not only can we swap our dumb_array's, but swaps in general can be more efficient; it merely swaps pointers and sizes, rather than allocating and copying entire arrays. Aside from this bonus in functionality and efficiency, we are now ready to implement the copy-and-swap idiom.

Without further ado, our assignment operator is:

dumb_array& operator=(dumb_array other) // (1)
{
swap(*this, other); // (2)

return *this;
}

And that's it! With one fell swoop, all three problems are elegantly tackled at once.

Why does it work?

We first notice an important choice: the parameter argument is taken by-value. While one could just as easily do the following (and indeed, many naive implementations of the idiom do):

dumb_array& operator=(const dumb_array& other)
{
dumb_array temp(other);
swap(*this, temp);

return *this;
}

We lose an important optimization opportunity. Not only that, but this choice is critical in C++11, which is discussed later. (On a general note, a remarkably useful guideline is as follows: if you're going to make a copy of something in a function, let the compiler do it in the parameter list.‡)

Either way, this method of obtaining our resource is the key to eliminating code duplication: we get to use the code from the copy-constructor to make the copy, and never need to repeat any bit of it. Now that the copy is made, we are ready to swap.

Observe that upon entering the function that all the new data is already allocated, copied, and ready to be used. This is what gives us a strong exception guarantee for free: we won't even enter the function if construction of the copy fails, and it's therefore not possible to alter the state of *this. (What we did manually before for a strong exception guarantee, the compiler is doing for us now; how kind.)

At this point we are home-free, because swap is non-throwing. We swap our current data with the copied data, safely altering our state, and the old data gets put into the temporary. The old data is then released when the function returns. (Where upon the parameter's scope ends and its destructor is called.)

Because the idiom repeats no code, we cannot introduce bugs within the operator. Note that this means we are rid of the need for a self-assignment check, allowing a single uniform implementation of operator=. (Additionally, we no longer have a performance penalty on non-self-assignments.)

And that is the copy-and-swap idiom.

What about C++11?

The next version of C++, C++11, makes one very important change to how we manage resources: the Rule of Three is now The Rule of Four (and a half). Why? Because not only do we need to be able to copy-construct our resource, we need to move-construct it as well.

Luckily for us, this is easy:

class dumb_array
{
public:
// ...

// move constructor
dumb_array(dumb_array&& other) noexcept ††
: dumb_array() // initialize via default constructor, C++11 only
{
swap(*this, other);
}

// ...
};

What's going on here? Recall the goal of move-construction: to take the resources from another instance of the class, leaving it in a state guaranteed to be assignable and destructible.

So what we've done is simple: initialize via the default constructor (a C++11 feature), then swap with other; we know a default constructed instance of our class can safely be assigned and destructed, so we know other will be able to do the same, after swapping.

(Note that some compilers do not support constructor delegation; in this case, we have to manually default construct the class. This is an unfortunate but luckily trivial task.)

Why does that work?

That is the only change we need to make to our class, so why does it work? Remember the ever-important decision we made to make the parameter a value and not a reference:

dumb_array& operator=(dumb_array other); // (1)

Now, if other is being initialized with an rvalue, it will be move-constructed. Perfect. In the same way C++03 let us re-use our copy-constructor functionality by taking the argument by-value, C++11 will automatically pick the move-constructor when appropriate as well. (And, of course, as mentioned in previously linked article, the copying/moving of the value may simply be elided altogether.)

And so concludes the copy-and-swap idiom.



Footnotes

*Why do we set mArray to null? Because if any further code in the operator throws, the destructor of dumb_array might be called; and if that happens without setting it to null, we attempt to delete memory that's already been deleted! We avoid this by setting it to null, as deleting null is a no-operation.

†There are other claims that we should specialize std::swap for our type, provide an in-class swap along-side a free-function swap, etc. But this is all unnecessary: any proper use of swap will be through an unqualified call, and our function will be found through ADL. One function will do.

‡The reason is simple: once you have the resource to yourself, you may swap and/or move it (C++11) anywhere it needs to be. And by making the copy in the parameter list, you maximize optimization.

††The move constructor should generally be noexcept, otherwise some code (e.g. std::vector resizing logic) will use the copy constructor even when a move would make sense. Of course, only mark it noexcept if the code inside doesn't throw exceptions.

When is copy-and-swap idiom not applicable

When should we avoid using the copy-and-swap idiom ?

When you can prove that naïve copy is safe and faster than swap.

You can identify the case when there are no member pointers (neither smart nor raw) that own objects and same is true for all member objects.

And when it "cannot be used" altogether?

Copy-and-swap cannot be used when the type is not swappable.

To be swappable, the type must be either move-constructible and move-assignable, or you must have defined a swap member function or a friend function.

Copy & Move Idiom?

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

Why does copy-and-swap in a base class cause the copy-assignment operator to be implicitly deleted in the derived class?

In your code, the derived copy assignment is not deleted. What is deleted though is the move assignment, because of [class.copy.assign]/7.4, which states that a defaulted move assignment operator is deleted if overload resolution for the move assignment on a base class is ambiguous.

The compiler wouldn't be able to tell whether to call operator=(base) or operator=(base&&) in order to move the base class.


This is always a problem, even if you try to move assign a base class object directly to another base class object. So it is not really practical to have both overloads. It is not clear to me why you need both. From what I can tell you can remove the operator=(base&&) overload without ill effect.

Should copy assignment operator leverage std::swap as a general rule?

Your code does not have a move constructor. Your copy constructor blocks the automatic creation of a move constructor, and attempting to move your class instead copies it.

For move-assign, your operator= also blocks its automatic implementation, and can be used in its place.

The end result is an infinite recursive call of = (live code).

If you follow the rule of 0, you need neither a copy constructor, move constructor, move-assignment, copy-assignment, or destructor. If you write any of them, you should be prepared to write all of them.

Using std::swap can be useful, but because you have to write your move-assign and move-construct, doing either in terms of std::swap is an infinite recursion waiting to happen.



Related Topics



Leave a reply



Submit