Writing a correct copy assignment operator for a class that manages a resource is harder than it looks. The naive version has at least three problems: it doesn't handle self-assignment, it leaves the object in a broken state if an allocation fails partway through, and it duplicates almost all of the logic already in the copy constructor and destructor. The copy-and-swap idiom solves all three at once by delegating to the copy constructor and a swap function. This chapter covers the problem in detail, the implementation step by step, why it gives the strong exception guarantee, how the same operator can also serve as the move assignment operator, the role of a non-member swap and argument-dependent lookup, the tradeoffs, and where modern C++ pushes you toward the rule of zero instead.
To see what copy-and-swap fixes, look at a class that owns a heap allocation: a small dynamic array of product prices.
This compiles and even runs correctly for typical uses. The bugs only show up in edge cases.
Bug 1: self-assignment corrupts the object. list = list; looks pointless, but it happens through references and aliases without the programmer realizing. Trace through what the naive operator does when &other == this:
delete[] data_; frees the storage other.data_ also points at.size_ = other.size_; is fine but other.size_ is now stale (same object).data_ = new double[other.size_]; allocates a new array.std::copy(other.data_, other.data_ + other.size_, data_); reads from other.data_, which is the just-deleted pointer.Step 4 is reading freed memory. The behavior is undefined: garbage values, a crash, or silent corruption.
Bug 2: allocation failure leaves the object broken. If step 3 throws std::bad_alloc, the object has already lost its old data in step 1 but doesn't have new data yet. data_ is left pointing at the freed memory. The destructor will eventually run and call delete[] on that dangling pointer, which is undefined behavior. The object is unusable from the moment the exception fires.
Bug 3: duplicated logic. Steps 2 through 4 replicate what the copy constructor already does. Step 1 replicates what the destructor does. Every change to those two functions has to be mirrored in the assignment operator. That's a maintenance trap.
A self-assignment check (if (this == &other) return *this;) fixes Bug 1, but does nothing for Bugs 2 and 3. The copy-and-swap idiom fixes all three.
The idiom builds on two functions that already exist in any well-written class: a copy constructor that makes a correct, complete copy of the object, and a destructor that cleans up. The assignment operator can delegate to both. It also needs a swap function that exchanges the internals of two objects without throwing.
The full pattern looks like this:
Output:
The assignment operator now takes its parameter by value, not by const reference. The body is two lines: call swap, return *this. There's no delete, no new, no self-assignment check, no copy logic.
Walking through what happens for b = a;:
other from a. other is a fresh, independent copy.swap(*this, other) exchanges the internals. *this now holds the data that was just copied from a. other now holds the original guts of *this.other goes out of scope and its destructor runs, freeing the old data that used to be in *this.All three bugs are gone:
b = b; still calls the copy constructor first to make other. The copy is independent of b. The swap exchanges b's data with the copy's data, and the destructor of other cleans up. No double-free, no use-after-free.*this is touched at all. *this is exactly as it was before the call. This is the strong exception guarantee: either the operation succeeds, or the object is unchanged.swap handles the rest. The assignment operator itself is a two-liner.The pass-by-value parameter goes against the usual rule of "pass non-trivial parameters by const reference to avoid copying." It's deliberate: the copy has to happen somewhere, and the parameter slot is a good place for it.
Compare the two shapes:
Both are correct. The pass-by-value version is shorter, but the bigger benefit is that the compiler can elide the copy in some cases. When the caller passes an rvalue (a temporary), the parameter doesn't need a copy at all; the compiler can construct other directly in place by calling the move constructor. The pass-by-const-reference version always copies, because the explicit PriceList temp(other); line forces it.
This sets up the second benefit: copy-and-swap with a by-value parameter also serves as the move assignment operator.
C++11 added move semantics. A class that owns a resource typically wants two assignment operators: copy assignment (PriceList& operator=(const PriceList&)) and move assignment (PriceList& operator=(PriceList&&)). Copy-and-swap collapses them into one.
The reason is overload resolution combined with by-value parameters. The compiler picks how to construct the parameter other based on what the caller passes:
| Caller's expression | How other is built |
|---|---|
b = a; (lvalue) | Copy constructor: other is a copy of a |
b = makePriceList(); (prvalue) | Move constructor (or elision): other steals from the temporary |
b = std::move(a); (xvalue) | Move constructor: other steals from a |
In all three cases, the body of operator= is the same: swap and return. The expensive part of each case happens during parameter construction. For an lvalue argument, that's a copy. For an rvalue, it's a move, which for PriceList is just swapping a pointer and a size: no allocation.
So the single signature PriceList& operator=(PriceList other) correctly handles both copy assignment and move assignment with no additional code. The compiler picks the right constructor for the parameter, and the rest is shared. This is one of the reasons copy-and-swap remained popular after move semantics arrived.
Some classes still prefer separate operator=(const T&) and operator=(T&&) for performance, but for most resource-owning classes, the unified form is clean enough that the small extra allocation on rvalues (when a separate move-assignment could have been more efficient) doesn't matter.
The swap function has a few important details. It's defined as a non-member friend of the class, and it's spelled swap, not Swap or SwapPriceList.
Non-member. Defining swap as a free function (declared as a friend inside the class so it has access to private members) makes it discoverable through argument-dependent lookup (ADL). When generic code says swap(x, y) where x and y are PriceList, the compiler searches the namespace of PriceList for an unqualified swap. If it finds one, that's the one it calls. This is how the standard library's algorithms find your custom swap without knowing the class name.
The standard library is full of code that calls swap(a, b) generically: std::sort, std::rotate, std::reverse, and every container's swap member. All of them rely on ADL to find your specialized version. If you only define a member swap (or only a static method), ADL won't find it and the standard library falls back to std::swap, which uses move construction and move assignment. For a class that hasn't defined move operations yet, that fallback uses copy operations, which is exactly the inefficiency you wanted to avoid by defining swap at all.
The `using std::swap;` line. Inside the swap body, the call swap(a.size_, b.size_) swaps two size_t members. There's no custom swap for size_t, so the call has to find std::swap. The pattern using std::swap; swap(x, y); tells the compiler: "look for a swap via ADL first, and fall back to std::swap if none is found." This is the canonical ADL pattern and it appears everywhere generic C++ code calls swap.
`noexcept`. Marking swap as noexcept is essential. The whole exception-safety story of copy-and-swap depends on swap not throwing. If swap could throw, then a failure mid-swap would leave the two objects in some indeterminate intermediate state, and the strong guarantee would be lost. For a class that swaps members of built-in types (pointers, integers) or types that themselves have noexcept swaps, this is automatically true. Annotating it explicitly documents the guarantee and lets the standard library use stronger algorithms (move-aware sorts, exception-aware allocator behaviors) that depend on noexcept swap.
ADL is one of the more subtle parts of C++ name resolution. The version that matters for swap is: when the compiler encounters an unqualified call like swap(x, y), it looks for a swap not just in the current scope and the surrounding namespaces, but also in the namespaces and classes associated with the types of x and y. A free function swap declared next to a class definition (or as a friend inside it) is automatically discoverable from any code that has the type as an argument.
The using std::swap; brings std::swap into scope as a fallback. The unqualified call swap(a, b) then does ADL first. Since Cart lives in shop, the compiler also searches shop for a swap, and it finds the one declared as a friend. The custom swap wins because in overload resolution, an exact match (taking Cart&) beats a template (taking T&).
If Cart didn't have a custom swap, ADL would find nothing, and the call would fall back to std::swap brought in by the using declaration. That fallback uses move construction and move assignment of Cart, which is correct but less efficient than a swap of internal pointers.
The reason this design exists is that the standard library can call swap generically without knowing the name of your class, and your class can plug a faster implementation into the same call site by defining a friend swap. This is the same mechanism that lets users customize begin, end, size, and other library-aware operations.
Copy-and-swap is not free. The pass-by-value parameter forces a copy on every lvalue assignment, even when the destination already has the right size or shape and could have reused its storage. Compare:
If this already has a buffer of exactly the right size, the manual version does zero allocations: it just copies the elements into the existing storage. The copy-and-swap version always allocates a new buffer (in the parameter's copy constructor), swaps it in, and then frees the old buffer when the parameter goes out of scope. That's one extra allocation and one extra deallocation per assignment, even in the case where no allocation was logically necessary.
For some workloads this overhead is real. A container that's resized in a tight loop, an inner-loop matrix operation, or a hot path that assigns to fixed-size objects can all measure the cost. For most code, the overhead is invisible and the safety and simplicity of copy-and-swap win.
The decision tree:
| Situation | Use |
|---|---|
| Resource-owning class, normal usage | Copy-and-swap |
| Performance-critical, profile shows allocation cost dominates | Manual assignment with self-assignment check and strong guarantee |
| Class only contains members that handle themselves (smart pointers, containers) | Rule of zero: let the compiler generate everything |
The third row is the modern default for new code, covered next.
The best way to deal with assignment operators is to not write one at all. The rule of zero says: if every member of your class manages its own resources (e.g., std::vector, std::string, std::unique_ptr, std::shared_ptr), the compiler-generated default copy constructor, copy assignment operator, move constructor, move assignment operator, and destructor will all do the right thing. You can delete the boilerplate.
The PriceList example using rule of zero:
Output:
There is no destructor, no copy constructor, no copy assignment operator, no move operations. The std::vector<double> member handles all of that internally. The compiler-generated functions correctly copy, move, and destroy the vector, which means they correctly copy, move, and destroy the PriceList.
The rule of zero is the default for new code. Use a standard container, a smart pointer, or another well-designed value type for each resource, and let the language do the work. You only write a destructor, copy constructor, copy assignment, move constructor, or move assignment when you're managing a raw resource that the standard library doesn't already wrap. By the time you need any of those, you usually need all five, which is the rule of five, and copy-and-swap is one good way to handle the assignment operators inside the rule of five.
The idiom has limits. A few situations where it doesn't apply:
operator= slices the derived part. The usual fix in this case is to delete copy assignment on the base entirely and provide clone() instead.Outside of these cases, copy-and-swap remains the canonical way to write a correct copy assignment operator for a resource-owning class, and the rule of zero remains the way to avoid writing one at all.
10 quizzes