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:
- A working copy constructor that duplicates resources and may throw exceptions.
- A working destructor that safely releases acquired resources.
- A
noexceptswap 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.