In C++, classes that manage resources usually implement the copy constructor, destructor and copy assignment operator according to the Rule of Three. If move semantics are desired, the move constructor and move assignment operators have to be implemented additionally in accordance with the Rule of Five. The Copy-and-Swap Idiom is a useful pattern to avoid code duplication in these methods and ensures strong exception safety without having an impact on the performance (for most cases). Here I’ll give a short overview how to implement this pattern.

Background

First let’s recap some basic concepts (just for the sake of completeness).

Rule of Three: If you need to explicitly declare either the destructor, copy constructor or copy assignment operator yourself, you probably need to explicitly declare all three of them.

Rule of Five: For each class that desire move semantics, we need to implement the move constructor and move assignment operator in addition to the methods specified by the Rule of Three.

Basic Exception Safety: Operations can fail which can result in side effects. However all invariants are preserved and there are no memory leaks.

Strong Exception Safety: Operations can fail and if they do, it’s guaranteed to have no side effects. This means the original object leaves its values intact.

Getting started

As an example I’ll create a simple string class and show how the copy-constructor, destructor and copy-assignment operator can be implemented:

class MyString
{
private:
    std::size_t size;
    char* str;

The default constructor can be implemented straight forward as well as the copy-constructor and destructor:

public:
    MyString()
        : size(other.size)
        , str(new char[other.size])
    {
    }

    MyString(const MyString& other)
        : size(other.size)
        , str(new char[other.size])
    {
        std::copy(other.str, other.str + size, str);
    }

    ~MyString()
    {
        delete[] str;
    }

How not to do it

So far this looks quite good, but we miss the copy-assignment operator. Let’s first demonstrate how not to do it:

    MyString& operator=(const MyString& other)
    {
        // (1) Consider self-assignment
        if (this != other)
        {
            // (2) Delete old data
            delete[] str;
            str = nullptr;

            // (3) Copy new data
            size = other.size;
            str = new char[other.size];
            std::copy(other.str, other.str + other.size, str);
        }
        return *this;
    }

With this solution we get the following problems:

  1. Explicitely checking for self assignment everytime can slow down the code, because usually self-assignemnt rarely happens.
  2. This copy-assignment operator only ensures basic exception safety. If the new command fails to allocate enough memory the object might be left in an invalid state.

To guarantee strong exception safety we could rewrite the code a little bit. First store the new data in local variables, then delete the allocated memory and finally replace the pointers (or values respectively) to the old data with (pointers to) the new data. Like this:

    MyString& operator=(const MyString& other)
    {
        // (1) Consider self-assignment
        if (this != other)
        {
            // (2) Copy new data to local variables
            size_t newSize = other.size;
            char* newStr = new char[other.size];
            std::copy(other.str, other.str + other.size, newStr);

            // (3) Delete allocated memory
            delete[] str;

            // (4) Replace old values/pointers
            size = newSize;
            str = newStr;
        }
        return *this;
    }

This leads to another problem: Code duplication in the copy assignment operator and the constructor. In this simple example it’s not that big of a deal, but with bigger datastructures it can be significant. This leads to the…

Copy-and-Swap Idiom

As an intermediary step we implement a swap-function:

    friend void swap(MyString& o1, MyString& o2)
    {
        using std::swap; // Allows for argument-dependent lookup (ADL) and
                         // therefore makes swapping more efficient.
        swap(o1.size, o2.size);
        swap(o1.mArray, o2.mArray);
    }

We can now use the swap method to implement the move constructor

    MyString(MyString&& other) noexcept
        : MyString()
    {
        swap(*this, other);
    }

as well as the assignment operator

    MyString& operator=(MyString other)
    {
        swap(*this, other);
        return *this;
    }
};

The interesting part here is that we don’t need to define a seperate copy- and move assignment operator. Instead we pass by-value and let the compiler decide whether the formal parameter other is copy- or move constructed. A nice sideffect is that copying by-value allows the compiler to perform optimizations by the use of Copy Elision. Passing by const MyString& would prevent the compiler from eliding copies. As a consequence of using the Copy-and-Swap idiom the Rule of Five becomes the Rule of Four (and a half) because we don’t have to implement two different assignment operators.

Conclusion

As shown above, the Copy-and-Swap idiom provides a means to reduce redundancies in methods required due to the Rule of Five without introducing a noticable impact on performance. Because copy/move constructors- and assignment operators are frequently implemented in classes with custom resource management, the Copy-and-Swap Idiom is a quite useful pattern to have at one’s hand.

Sources

  • https://stackoverflow.com/questions/3279543/what-is-the-copy-and-swap-idiom
  • https://riptutorial.com/cplusplus/example/10742/copy-and-swap
  • https://web.archive.org/web/20140113221447/http://cpp-next.com/archive/2009/08/want-speed-pass-by-value/