C++ Copy-and-Swap Idiom for Strong Exception Safety

The copy-and-swap idiom is an established C++ design pattern that enables resource-managing classes to implement assignment operators with strong exception safety and minimal code duplication. By leveraging a class's copy constructor to handle dynamic allocations before mutating the target object, and subsequently exchanging resources using a non-throwing swap function, the idiom guarantees that if an operation fails, the original object remains untouched. This approach provides "commit-or-rollback" semantics, elegantly resolves self-assignment issues, and unifies copy and move assignment logic into a single robust operator.

The Exception Safety Challenge in Assignment Operators

Strong exception safety guarantees that if an exception is thrown during an operation, the program's state remains exactly as it was before the operation began. In assignment operators, achieving this guarantee using traditional approaches is notoriously error-prone.

Consider a naive implementation of an assignment operator that manually manages a dynamic resource:

MyClass& MyClass::operator=(const MyClass& other) {
    if (this != &other) {
        delete m_data;          // Old resource is destroyed
        m_data = new int(*other.m_data); // Can throw std::bad_alloc
    }
    return *this;
}

If new int throws an exception, m_data points to deallocated memory or remains undefined. The left-hand side object is left in a corrupted state, violating the strong exception guarantee and leading to undefined behavior when the object is eventually destroyed.

Mechanics of the Copy-and-Swap Idiom

The copy-and-swap idiom resolves this vulnerability by splitting the assignment operation into two distinct phases: a potentially throwing copy phase and an entirely non-throwing swap phase.

The idiom relies on three core components:

  1. A working copy constructor that duplicates resources and may throw exceptions.
  2. A working destructor that safely releases acquired resources.
  3. A noexcept swap function that exchanges the internal member variables of two objects without allocating memory or throwing exceptions.

The assignment operator is then implemented by accepting the parameter by value rather than by const reference:

class ResourceHolder {
private:
    int* data;
    size_t size;

public:
    // Constructor
    ResourceHolder(size_t s = 0) 
        : data(s ? new int[s]() : nullptr), size(s) {}

    // Destructor
    ~ResourceHolder() {
        delete[] data;
    }

    // Copy Constructor (May throw std::bad_alloc)
    ResourceHolder(const ResourceHolder& other)
        : data(other.size ? new int[other.size]() : nullptr), size(other.size) {
        if (data) {
            std::copy(other.data, other.data + size, data);
        }
    }

    // Move Constructor
    ResourceHolder(ResourceHolder&& other) noexcept
        : data(nullptr), size(0) {
        swap(*this, other);
    }

    // Non-throwing swap function
    friend void swap(ResourceHolder& first, ResourceHolder& second) noexcept {
        using std::swap;
        swap(first.data, second.data);
        swap(first.size, second.size);
    }

    // Unified Assignment Operator using Copy-and-Swap
    ResourceHolder& operator=(ResourceHolder other) noexcept {
        swap(*this, other);
        return *this;
    }
};

How Copy-and-Swap Guarantees Strong Exception Safety

1. Pre-execution Allocation (The Copy Step)

When invoking a = b;, the argument other is initialized by value before the body of operator= begins executing. This pass-by-value invokes the copy constructor. If resource allocation fails (for example, std::bad_alloc is thrown), the exception occurs entirely outside operator=. The state of object a has not been modified, ensuring a complete, automatic rollback.

2. Atomic Commit (The Swap Step)

Once other is successfully constructed, execution enters the body of operator=. The swap function performs shallow, non-allocating swaps of pointers and primitive types. Because swap is marked noexcept and performs only primitive exchanges, it is mathematically incapable of throwing. This effectively commits the new state to *this.

3. Automatic Cleanup

After swapping, *this holds the new resources, and the local parameter other holds the old resources that originally belonged to *this. When operator= returns, other falls out of scope, invoking its destructor. The old resources are cleaned up cleanly and deterministically.

Additional Architectural Benefits

  • Elimination of Self-Assignment Checks: Traditional assignment requires if (this != &other) checks to prevent self-destruction. In copy-and-swap, assigning an object to itself creates an independent local copy first, swapping into it and destructing the duplicate safely without branching.
  • Unified Copy and Move Assignment: By passing the parameter by value, the single operator handles both lvalues and rvalues. Passing an lvalue invokes the copy constructor, while passing an rvalue (such as from std::move) invokes the move constructor, avoiding redundant code implementations.
  • DRY Principle Enforcement: The assignment operator avoids re-implementing memory allocation (handled by the copy constructor) and deallocation (handled by the destructor), keeping resource management logic centralized.