Skip to content
C++
C++11Intermediate

Copy-and-Swap Idiom

Writing a correct copy assignment operator is surprisingly tricky: you must handle self-assignment, provide the strong exception guarantee (if the copy fails, the original is unchanged), and not leak the old resources. The copy-and-swap idiom solves all three problems at once by delegating to the copy constructor (which you already wrote) and a swap function (which is noexcept). The result is a single assignment operator that is correct, exception-safe, and self-assignment-safe by construction.

The problem with naive copy assignment

Consider a simple class that owns a heap buffer. A naive copy assignment has three failure modes: it leaks the old buffer if the copy allocates first and then the old delete crashes; it corrupts state on self-assignment (a = a); and it leaves the object in a partially modified state if the copy throws after partial progress.

class Buffer {
public:
    Buffer(std::size_t n) : data_(new int[n]), size_(n) {}
    ~Buffer() { delete[] data_; }

    // NAIVE — has problems:
    Buffer& operator=(const Buffer& other) {
        delete[] data_;           // frees old memory
        size_ = other.size_;
        data_ = new int[size_];   // if this throws, object is broken!
        std::copy(other.data_, other.data_ + size_, data_);
        return *this;
    }

    // Also broken for self-assignment:
    // Buffer b(10); b = b;  — deletes b.data_, then reads from it!

private:
    int* data_;
    std::size_t size_;
};

The copy-and-swap solution

The pattern: (1) write a correct copy constructor, (2) write a swap that exchanges all members and is noexcept, (3) implement copy assignment by taking the argument by value (invoking the copy constructor) then swapping with *this. The old resources are released when the local copy is destroyed at the end of the function.

class Buffer {
public:
    Buffer(std::size_t n) : data_(new int[n]), size_(n) {}

    // 1. Copy constructor: copies data
    Buffer(const Buffer& other) : data_(new int[other.size_]), size_(other.size_) {
        std::copy(other.data_, other.data_ + size_, data_);
    }

    // 2. Destructor
    ~Buffer() { delete[] data_; }

    // 3. swap — noexcept, just exchanges pointers
    friend void swap(Buffer& a, Buffer& b) noexcept {
        using std::swap;
        swap(a.data_, b.data_);
        swap(a.size_, b.size_);
    }

    // 4. Copy assignment via copy-and-swap:
    //    'other' is constructed by copy — any exception happens HERE, before *this is touched
    Buffer& operator=(Buffer other) noexcept {  // take by VALUE
        swap(*this, other);   // exchange: *this gets other's data, other gets old data
        return *this;
        // other (now holding old data) is destroyed here → delete[] cleans up old buffer
    }

private:
    int* data_;
    std::size_t size_;
};

Why this is correct: the copy is made before any modification to *this. If the copy constructor throws, *this is completely untouched — the strong exception guarantee is automatic. Self-assignment (a = a) makes a copy of a, swaps it in, then deletes the copy — correct and safe.

Move assignment falls out naturally

Because the assignment operator takes its argument by value, passing an rvalue causes the move constructor to be called instead of the copy constructor — the argument is constructed cheaply by move, then swapped in. You get efficient move assignment without writing a separate operator.

Buffer a(100);
Buffer b(200);

b = a;             // lvalue: copy constructor called for 'other', then swap
b = std::move(a);  // rvalue: move constructor called for 'other' (cheap!), then swap

// The single 'operator=(Buffer other)' handles both cases.

The trade-off: for copy assignment, you always pay for a copy (even if assigning to a buffer of the same size that could reuse memory). Some performance-critical types write separate copy and move assignment operators to avoid this. The copy-and-swap idiom trades that optimization opportunity for correctness and simplicity.

Key rules to remember

Copy-and-swap gives the strong exception guarantee for free

The copy happens before *this is modified. If the copy throws, the original is unchanged. This is exactly the strong guarantee.

Self-assignment is automatically handled — no if (this == &other) check needed

Self-assignment makes a copy of the same object and swaps it in, which is correct though slightly wasteful.

swap must be noexcept — it only exchanges pointers and primitives

The assignment operator itself becomes noexcept when swap is noexcept. This enables important optimizations in containers.

A single by-value operator= handles both copy and move assignment

If the caller passes an lvalue, the copy constructor runs. If an rvalue, the move constructor runs. The same operator= body handles both.

Sign in to track progress