OSC++.025: Copy-and-Swap and Exception-Safe Assignment — Strong Guarantee, noexcept swap, Self-Assignment, and Tradeoffs

Two developers studying C++ copy-and-swap and the strong exception guarantee on dual monitors, with code, object-state diagrams, and a commit flow.

Elementary Overview

Copy assignment is harder than it first appears because the destination object already owns a valid state before assignment begins. If copying the new state fails halfway through, the class must not leak resources or leave the destination corrupted. The copy-and-swap idiom solves this by preparing a complete temporary copy first, then committing the new state with a non-throwing swap.

This lesson continues directly from OSC++.024: Rule of Five and Rule of Zero, OSC++.023: Perfect Forwarding, OSC++.022: Move Semantics, and OSC++.021: Smart Pointers and Ownership. The focus is one operation: designing assignment so failure does not partially destroy a valid object.

What You Should Learn

  • Why copy assignment has more failure paths than copy construction.
  • What the basic, strong, and no-throw exception guarantees mean.
  • How copy-and-swap turns assignment into prepare-then-commit.
  • Why the copy step may throw while swap should normally be noexcept.
  • How copy-and-swap naturally handles self-assignment.
  • Why pass-by-value assignment can combine copy and move assignment logic.
  • When copy-and-swap is elegant but not necessarily the fastest implementation.

The Core Copy-and-Swap Pattern

A compact modern form takes the right-hand side by value. Constructing that parameter performs either a copy or a move before the function body commits anything to the destination. Inside the function, a non-throwing swap exchanges the destination state with the prepared temporary.

class Buffer {
public:
    Buffer& operator=(Buffer other) {
        swap(other);
        return *this;
    }

    void swap(Buffer& other) noexcept {
        using std::swap;
        swap(data_, other.data_);
        swap(size_, other.size_);
    }

private:
    std::unique_ptr<int[]> data_;
    std::size_t size_{};
};

The temporary parameter other first acquires the incoming value. If constructing that temporary throws, the assignment function body never commits a partial change. If construction succeeds, swap exchanges the complete states. When other goes out of scope, its destructor releases the destination’s former resources.

CppNuts — Copy And Swap Idiom In C++. An English walkthrough of implementing assignment with a temporary object and swap for simpler, exception-safe resource management.
Original diagram showing the C++ copy-and-swap strong exception guarantee: original object, temporary copy, noexcept swap, commit, and a failure path that leaves the original object unchanged.
Original BitcoinVersus.Tech diagram: copy-and-swap separates preparation from commit. If the temporary copy fails, the original object remains unchanged; after preparation succeeds, a non-throwing swap commits the new state.

Understand The Exception Guarantees

Exception safety is not the claim that a function can never fail. It describes what remains true after failure. Microsoft’s modern C++ guidance describes three common levels: the basic guarantee, the strong guarantee, and the no-throw guarantee. The basic guarantee keeps objects valid and prevents leaks. The strong guarantee behaves like a transaction: either the operation succeeds, or the observable state remains unchanged. The no-throw guarantee promises that the operation itself does not allow exceptions to escape.

Copy-and-swap is attractive because the potentially throwing work happens while creating the temporary. The existing destination object is not modified until the swap step. If swap is truly non-throwing, that final commit can provide the strong guarantee.

CppCon 2019 — Ben Saks, “Back to Basics: Exception Handling and Exception Safety.” An English explanation of exception guarantees, RAII, throwing/catching practices, and designing code that remains valid when operations fail.

The Copy Step Can Throw

Do not mark the pass-by-value assignment operator noexcept merely because its internal swap is noexcept. Creating the by-value parameter may invoke a copy constructor, and copying may allocate memory or copy members whose constructors can throw. The important guarantee is that this failure happens before the destination is changed.

Buffer& operator=(Buffer other) {
    // If copying into 'other' failed, execution never reached here.
    swap(other);      // should not throw
    return *this;
}

The swap function, by contrast, should normally exchange members whose own swaps are non-throwing. Standard-library resource handles such as std::unique_ptr are especially useful here because their swaps are designed for inexpensive ownership exchange.

English r/cpp_questions discussion: “What are the benefits of the copy-and-swap technique?” The thread directly discusses automatic exception safety, self-assignment, temporary ownership, and the performance tradeoff versus reusing an existing allocation.

Why Self-Assignment Works Naturally

Traditional hand-written copy assignment often checks if (this == &rhs) because deleting the destination’s resource before copying from the source can destroy the very data being copied. Copy-and-swap does not depend on that fragile ordering. The temporary copy is complete before the destination’s old state is exchanged, so an expression such as a = a; remains valid without a special early-return branch.

Pass By Value Can Reuse Move Construction

The by-value parameter also gives the compiler a useful choice. When assignment receives an lvalue, constructing other uses the copy constructor. When it receives an rvalue, the parameter can be move-constructed instead. The same assignment body can therefore accept both copied and moved input states.

Buffer a;
Buffer b;

a = b;             // parameter is copied from b
a = Buffer{1024};  // parameter can be moved from the temporary

This compact design is elegant, but it should not be mistaken for a universal performance rule. A specialized copy-assignment operator may be able to reuse an existing allocation rather than always creating a new temporary resource.

Howard Hinnant — “Everything You Ever Wanted to Know About Move Semantics.” This English talk examines special-member-function design and the tradeoffs of copy-and-swap, including cases where separate assignment logic can reuse storage more efficiently.

Copy-and-Swap Is About Correctness First

Copy-and-swap is often taught because it reduces duplicated cleanup logic and makes exception safety easier to reason about. It is not automatically faster than a carefully written assignment operator. If a destination already owns a large buffer with sufficient capacity, a specialized copy assignment may reuse that memory and avoid a fresh allocation. Howard Hinnant has specifically highlighted that copy-and-swap can impose measurable overhead in classes containing reusable resources such as vectors or strings.

The design decision is therefore contextual: use copy-and-swap when its strong safety, compactness, and maintainability are worth the temporary. Prefer Rule of Zero when standard-library members can manage resources automatically. Write specialized assignment only when the resource model and measurements justify the added complexity.

A More Explicit Strong-Guarantee Assignment

You can also preserve the prepare-then-commit idea without using the pass-by-value form for every assignment. One approach constructs replacement resources first, then swaps or installs them only after all throwing work succeeds.

Buffer& Buffer::operator=(const Buffer& rhs) {
    if (this == &rhs) {
        return *this;
    }

    Buffer replacement(rhs);  // may throw; *this is unchanged
    swap(replacement);        // commit, ideally noexcept
    return *this;
}

This form makes the strong guarantee explicit and lets copy assignment remain a distinct operation from move assignment if the class benefits from specialized behavior.

Design Checklist

  1. Prefer Rule of Zero when standard-library members already provide correct ownership.
  2. If custom assignment is required, identify every operation that may throw.
  3. Do not destroy the valid old state until replacement state is ready.
  4. Keep swap cheap and noexcept when the member types permit it.
  5. Verify self-assignment remains safe.
  6. Verify the object remains valid after allocation or copy failure.
  7. Measure before assuming copy-and-swap is faster or slower.
  8. Document whether assignment offers the basic or strong exception guarantee.

Practical Exercise

  1. Create a class that owns a dynamically allocated array and write a correct copy constructor and destructor.
  2. Implement copy assignment manually by allocating replacement storage before releasing the old storage.
  3. Implement a second version using copy-and-swap.
  4. Test a = a; for both implementations.
  5. Force an allocation failure or use a member type whose copy constructor throws, then verify the destination object remains valid.
  6. Replace the raw allocation with std::vector and determine whether Rule of Zero eliminates the custom assignment code entirely.
  7. Benchmark repeated assignments where the destination already has reusable capacity and compare specialized assignment with copy-and-swap.

Knowledge Check + Answers

  1. What is the main idea of copy-and-swap? Build a complete replacement first, then commit it with swap.
  2. What does the strong exception guarantee mean? If the operation fails, the observable state remains unchanged.
  3. Can constructing the temporary copy throw? Yes. That is acceptable because the destination has not yet been modified.
  4. Why should swap normally be noexcept? The commit step should not introduce a new failure after replacement state is ready.
  5. Does copy-and-swap require a manual self-assignment check? Usually no; preparing a separate temporary makes self-assignment naturally safe.
  6. Is copy-and-swap always the fastest assignment strategy? No. A specialized assignment operator may reuse existing storage and avoid temporary allocation.
  7. What design should usually be considered before custom copy-and-swap? Rule of Zero with RAII-aware standard-library types.

Reference Resources

Elementary Review

Copy-and-swap treats assignment like a small transaction. Prepare a valid replacement first. If preparation fails, keep the old object. If preparation succeeds, commit with a non-throwing swap. This makes correctness and exception safety easier to reason about, while Rule of Zero remains the preferred destination whenever standard-library types can manage the resource for you.

Editor’s Note

The featured image is an original 1200×630 BitcoinVersus.Tech cover created specifically for OSC++.025 and is not reused in the body. The body uses a separate original 1200×675 English instructional diagram. The lesson contains three unique English-language YouTube videos implemented as responsive native Gutenberg 16:9 embed blocks and one directly relevant English-language social-media embed. Ordinary lesson prose is not placed inside bordered, shaded, card, callout, panel, or fixed-width text boxes.

BitcoinVersus.Tech content is provided for informational and educational purposes.

Leave a Reply