The Copy and Swap Idiom

Posted: August 27, 2014

Before we move into discussing move semantics (the next step on my quest to convert you all to teaching in C++14), let’s clear up something that I often see being taught in a subtly “wrong” way with respect to memory management in even C++98.

Consider the following class, which might be written by a student in a fundamental data structures course:

template <class T>
class dynamic_array
{
  public:
    dynamic_array();

    dynamic_array(size_t);

    dynamic_array(const dynamic_array&);

    dynamic_array& operator=(const dynamic_array&);

    ~dynamic_array();

    // other functions, e.g. operator[], push_back(), push_front(), size(), etc.

  private:
    T* storage_;
    size_t size_;
    size_t capacity_;
};

What we see here is standard fare: we have a class that utilizes dynamic memory (via the storage_ pointer), and thus is provides the “Big Three”: a copy constructor, an assignment operator, and a destructor. These three overloads give us the value semantics that we desire, making it so that our class can “blend in” with the built in types of the language and standard library. C++ is rooted in value semantics, so it’s crucial that we get this right so that our classes are well behaved.

But let’s look more closely at our assignment operator. You may have seen it written something like this:

template <class T>
dynamic_array<T>& dynamic_array<T>::operator=(const dynamic_array& rhs)
{
    if (this != &rhs)
    {
        clear();
        copy(rhs);
    }
    return *this;
}

where clear() is a helper function for releasing the memory associated with the current object, and copy() is another helper function that is responsible for creating the memory for a new independent copy of the parameter, assigning it into the current object.

There are actually a couple of problems with this approach.

Pedagogical Complaints

Because the language semantics are often taught very early on in the course, and (at least at UIUC) to fairly inexperienced programmers (through no fault of their own: it’s just their second programming experience in the curriculum in its current form), you have to dance around this issue of the “self assignment” check.

this != &rhs: an enigma for “green” students

this != &rhs is particularly nuanced for students still struggling to understand some of the fundamental differences between Java and C++ (or C and C++). This requires them to understand:

  • the type of this (a pointer to the current instance)

  • the purpose of &rhs as getting a pointer to the argument (not a reference, and understanding that rhs itself is not a pointer)

  • what it would mean if this == &rhs.

That’s quite a bit of information we’re expecting them to digest in just a short little snippet. But if you write your assignment operator this way, it’s such a critical moment: if they forget this check, they will have memory errors coming out of their ears.

Technical Complaints

However, that’s not the real meat of my argument. My real beef with this setup is that it is completely exception unsafe. And, unless you’re living in a fairytale world where you

  • never allocate to the free store, and
  • never use the standard library

ignoring exceptions will be a fatal mistake at some point in your experience with C++.

And, please don’t come to me claiming that your codebase “doesn’t throw exceptions”, because you and I both know that’s just a load of crock. =)

Nearly every useful program is going to at least do one (and, likely, both) of the above two things. This means you have to care about exceptions.

Exception Safety

So what’s “unsafe” about this code?

Patience, young padawan. Let’s take a step back.

First, let’s identify where exceptions could be thrown, and then define what we want our class to do in the event of an exception. This will define what kind of “exception safety guarantee” we want to provide.

One of the bullet points I made above (when I was being rude; sorry) was that the memory allocator can throw exceptions. How could that be the case? Let’s look at three fairly simple examples:

  • We’re out of memory. This causes a std::bad_alloc exception to be thrown from the call to new[] that we’ll be using to allocate our array.

  • A constructor for an element in the array throws during the new[] call.

  • The assignment operator for an element in the new array throws when we are copying the data.

So clearly, then, the line that invokes copy() has the potential to throw an exception. What would happen to our data structure in this case? There are a few cases:

  • It could be completely empty if the allocation itself fails (out of memory or a throwing constructor for T during the new[] call).

  • It could be partially-copied if the exception came from T::operator=() when copying the data.

So what can we do to deal with this exception?

Let me be clear here: our goal is not to handle the exception. What should the program do if it can no longer allocate heap memory, for example? That’s not something that our data structure should be deciding. So we’re not even going to try to catch and handle this error: instead, what we’re going to try to guarantee is something about the state of our class after the exception has been thrown—namely, that it is in the same state as it was before the assignment line that caused the exception.

Putting the safety back on our assignment operator

Using the template we had before, we could imagine rewriting it in the following way:

template <class T>
dynamic_array<T>& dynamic_array<T>::operator=(const dynamic_array<T>& rhs)
{
    if (this != &rhs)
    {
        T* nstorage = NULL;
        try
        {
            nstorage = new T[rhs.capacity_];
            std::copy(rhs.storage_, rhs.storage_ + rhs.size_, nstorage);
        }
        catch (...)
        {
            delete[] nstorage;
            throw;
        }

        delete[] storage_;
        storage_ = nstorage;
        capacity_ = rhs.capacity_;
        size_ = rhs.size_;
    }
    return *this;
}

Shield your eyes! The horror! The code has exploded, has a horrible try/catch block to handle the fact that std::copy() could throw during the assignment into the array (the cost of a generic type T here), and is now almost certainly above the threshold of beginner programmers.

But it is exception safe.

Back to the drawing board

The above monstrosity is clearly beyond what we want to teach. There’s no reason we shouldn’t be able to achieve both goals: the ease of understanding that came with the clear() then copy() version, and also providing the strong exception safety guarantee.

This is where the “copy and swap” idiom comes into play. (It’s worth noting that this idiom is even more useful in C++11/14, but we’ll get there later.)

We start with the following observations:

  • We want operator=() to create new memory that is a completely independent copy of the parameter.

  • We must release the memory associated with the current object.

  • The current object must refer to the new memory that we’ve created.

…what if I told you that we already wrote most of this by virtue of having a well defined class? A copy() helper? We have a copy constructor! Let’s see if we can’t use that as a form of “helper function”. Remember from the above code that we want the following chain of events:

  • Allocate memory
  • Copy over values
  • Free old memory
  • Refer to the new copy

and further note that there’s no reason we couldn’t do the last two in a different order (we’d just need a temporary).

Let’s first define a helper function that we’ll use in our implementation:

template <class T>
void dynamic_array<T>::swap(dynamic_array& other)
{
    // enable ADL---not truly necessary in this case, but this is good
    // practice
    using std::swap;

    swap(storage_, other.storage_);
    swap(size_, other.size_);
    swap(capacity_, other.capacity_);
}

To be a good citizen, let’s also define a non-member swap function for our class that just delegates to the helper above1:

template <class T>
void swap(dynamic_array<T>& first, dynamic_array<T>& second)
{
    first.swap(second);
}

And now consider the following implementation for the assignment operator:

template <class T>
dynamic_array<T>& dynamic_array<T>::operator=(dynamic_array rhs)
{
    swap(rhs);
    return *this;
}

Woah! We have two lines of code. There’s no way that gets us everything we need… right?

But it does.

  • We get the copy by virtue of the argument being passed by value.

  • If the copying fails (e.g., the copy constructor throws), our function is not run at all, so our class appears unchanged by the assignment because it truly didn’t happen.

  • Swapping with the value parameter accomplishes our resource release. Remember that any parameters passed by value are going to have their destructors invoked when the stack frame for that function is popped.

  • We don’t have to check for self assignment anymore, as that code path can now be handled without a special case. Yes, it is less efficient, but the point of checking for self assignment wasn’t as an optimization, it was a “please don’t crash my program” check.

The Catch

The only thing we have to guarantee now is that our copy constructor satisfies the basic exception guarantee (which is to say that it does not leak in the event of an exception), which isn’t too bad (though the code is still not ideal):

template <class T>
dynamic_array<T>::dynamic_array(const dynamic_array<T>& other)
    : storage_(new T[other.capacity_]),
      size_(other.size_),
      capacity_(other.capacity_)
{
    try
    {
        std::copy(other.storage_, other.storage_ + size, storage_); // (1)
    }
    catch (...)
    {
        delete[] storage_;
        throw;
    }
}

The nastiness here is because the marked line (1) could throw during T’s assignment operator.

In the general case, there are ways of avoiding the try/catch here, but I think this is a reasonable compromise for now. It’s worth noting at this point that if you were teaching operator= with the clear() then copy() style, your copy constructor was probably exception unsafe, too, so this isn’t just a reflection of some “complication” in the copy-and-swap idiom.

If you’re dealing with some type T that you know does not throw from its assignment operator (an assumption I’m willing to make when teaching novice programmers), then the code can be simplified to just:

template <class T>
dynamic_array<T>::dynamic_array(const dynamic_array<T>& other)
    : storage_(new T[other.capacity_]),
      size_(other.size_),
      capacity_(other.capacity_)
{
    std::copy(other.storage_, other.storage_ + size_, storage_);
}

We’ll revisit this later when we start talking about C++11/14 and show how just a simple language switch can ensure that we get the basic exception guarantee out of our copy constructor in the general case by only a one line change to the above initializer list!

Closing Thoughts: An exception-safe “Big Three” for intro programmers

Let’s recap what our code for the “Big Three” looks like now, including all of our helper functions:

template <class T>
class dynamic_array
{
  public:
    dynamic_array();

    dynamic_array(size_t);

    dynamic_array(const dynamic_array&);

    dynamic_array& operator=(dynamic_array);

    ~dynamic_array();

    void swap(dynamic_array& other);

    // other functions, e.g. operator[], push_back(), push_front(), size(), etc.

  private:
    T* storage_;
    size_t size_;
    size_t capacity_;
};

template <class T>
dynamic_array<T>::dynamic_array(const dynamic_array<T>& other)
    : storage_(new T[other.capacity_]),
      size_(other.size_),
      capacity_(other.capacity_)
{
    // students will likely use a raw loop here, and that's fine
    // we're also making the assumption that T::operator=() does not throw,
    // in case you have a student who's an aspiring language lawyer =)
    std::copy(other.storage_, other.storage_ + size_, storage_);
}

template <class T>
void dynamic_array<T>::swap(dynamic_array& other)
{
    std::swap(storage_, other.storage_);
    std::swap(size_, other.size_);
    std::swap(capacity_, other.capacity_);
}

template <class T>
dynamic_array<T>& dynamic_array<T>::operator=(dynamic_array rhs)
{
    swap(rhs);
    return *this;
}

template <class T>
dynamic_array<T>::~dynamic_array()
{
    delete[] storage_;
}

Advantages

  • Real world applicability: exceptions are everwhere, you need to know them and how to handle them

  • Simplified explanation for operator=(), using language concepts they’re learning as they are doing copy constructors anyway (pass by value)

  • Elimination of the self assignment check (self assignment is automatically valid in the copy-and-swap idiom)

  • A helper function that’s useful to the outside world: a.swap(b)

Disadvantages

  • Requires some discussion of what exceptions are, what they are used for, and why we care about them

  • If you are truly being careful, in C++98/03 you will need to have a try/catch block in the copy constructor (but not in C++11/14, more to come…)

Coming Up

Now that we know about the copy-and-swap idiom, in the next post I’m going to talk briefly about move semantics in C++11/14, and then we can move on to tackle what I teased at in the very first post in this series: that we can teach manual memory management in C++11/14 without losing out on any teaching opportunities compared to C++98/03, all the while simultaneously being more modern and encouraging students to write safe code.

Yell at me in the comments!

  1. This matters significantly less in C++11 for reasons we’ll get to (namely, move semantics), but it’s still a good idea. Given we aren’t targeting C++11 necessarily in this post, it’s good to make sure we’re doing it right. Thanks to Larry for pointing out the deficiency in the comments.