OSC++.026: Custom swap and ADL — using std::swap, noexcept, and Swappable Types

Two glowing data blocks swapping positions above a C++ programming workstation, representing custom swap and safe state exchange.

A good custom swap function exchanges an object’s state directly instead of rebuilding the object through expensive copy operations. In modern C++, the design is tied closely to argument-dependent lookup (ADL), using std::swap;, conditional noexcept, and the idea that generic code should find a type-specific swap when one exists.

This lesson continues from OSC++.025: Copy-and-Swap and Exception-Safe Assignment, OSC++.024: Rule of Five and Rule of Zero, and OSC++.023: Perfect Forwarding. The focus now is the commit operation that copy-and-swap depends on: a correct and efficient swap.

Learning Objectives

  • Explain why generic std::swap is not always the best operation for a user-defined type.
  • Write a member swap that exchanges resources directly.
  • Write a non-member swap in the same namespace as the type.
  • Explain how argument-dependent lookup finds the non-member overload.
  • Use the idiom using std::swap; swap(a, b); correctly.
  • Apply noexcept when every member swap is non-throwing.
  • Understand how custom swap supports copy-and-swap and generic library code.

What std::swap Does by Default

For a typical movable type, the general std::swap implementation exchanges two objects using move construction and move assignment. Conceptually, it behaves like this:

template<class T>
void generic_swap(T& a, T& b) {
    T temp = std::move(a);
    a = std::move(b);
    b = std::move(temp);
}

That is correct for many types. But a class that owns a pointer, file handle, socket wrapper, buffer, or other resource may be able to exchange its members more directly. A custom swap can avoid invoking the class’s higher-level assignment logic and can make the intended operation explicit.

C++ programming language logo.
C++ programming language logo by Adrien Memel, CC BY-SA 4.0, via Wikimedia Commons.

Start With a Member swap

Consider a class that owns a dynamic buffer through std::unique_ptr. A member swap can exchange the ownership handle and metadata directly:

class Buffer {
public:
    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 function exchanges the two data members. It does not allocate new storage and it does not copy the elements in the buffer. For ownership-oriented types, this can be the difference between exchanging a few machine words and rebuilding an entire resource.

CppCon’s Kostas Kyrimis explains argument-dependent lookup, customization points, and why qualified calls such as std::swap can bypass user-defined overloads.

Add a Non-Member swap in the Same Namespace

Generic code cannot assume that every class exposes a member named swap. The conventional customization is therefore a non-member function placed in the same namespace as the type:

namespace storage {

class Buffer {
public:
    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_{};
};

void swap(Buffer& a, Buffer& b) noexcept {
    a.swap(b);
}

} // namespace storage

Placing the non-member overload beside the class matters because ADL examines namespaces associated with the argument types. When a call uses two storage::Buffer objects, the compiler can consider storage::swap even if that function was not explicitly named with a namespace qualifier.

Why using std::swap Comes Before swap(a, b)

The standard idiom in generic code is:

using std::swap;
swap(a, b);

The first line makes the standard fallback visible. The second line is intentionally unqualified. That lets ordinary overload resolution and ADL choose a better type-specific swap if one exists. If no better overload is available, std::swap remains a candidate.

Writing std::swap(a, b) directly can bypass a non-member swap found only through ADL. For a user-defined type with an efficient specialized exchange operation, that may force a less appropriate fallback path.

Bo Qian walks through Koenig lookup, another name for argument-dependent lookup, and shows how C++ expands name lookup using the argument types.

Why noexcept Matters

A swap function is often used as the final commit step in exception-safe code. If all member swaps are non-throwing, the class-level swap should normally communicate that guarantee with noexcept. This is especially important when the function supports copy-and-swap assignment: the replacement object can be prepared first, then committed with a swap that cannot fail.

Do not declare a function noexcept merely because you want it to be safe. If an operation inside a noexcept function actually throws and the exception escapes, the program calls std::terminate. The declaration must match reality.

Make noexcept Follow the Member Types

For a generic wrapper, it can be useful to make the exception specification depend on whether the contained type is swappable without throwing:

template<class T>
class Box {
public:
    void swap(Box& other)
        noexcept(std::is_nothrow_swappable_v<T>)
    {
        using std::swap;
        swap(value_, other.value_);
    }

private:
    T value_;
};

Since C++17, traits such as std::is_swappable_v and std::is_nothrow_swappable_v make it easier to reason about whether a type supports the operation and whether the operation can throw.

Connect swap Back to Copy-and-Swap

The previous lesson used a pass-by-value assignment operator:

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

That pattern works well only when the final swap really is cheap and non-throwing. The expensive or potentially throwing work occurs while constructing other. Once that succeeds, state exchange commits the new value and the temporary later destroys the old state.

A badly designed swap that calls the same assignment operator again can create recursion. That is why a custom swap should normally exchange the data members directly instead of implementing itself through whole-object assignment.

Related Social Discussion

Related social discussion: Naresh Jagadeesan discusses copy-and-swap, a friend/non-member swap, ADL, using std::swap, and noexcept.

std::ranges::swap in C++20

C++20 adds std::ranges::swap, a customization-point object that is designed to find appropriate swap behavior in generic code. Its lookup rules differ in detail from directly calling std::swap, but the engineering goal is similar: use an efficient user-provided exchange operation when one is available and fall back safely when it is not.

#include <utility>

std::ranges::swap(a, b);

For modern ranges-oriented generic code, this form can be clearer than manually reproducing the traditional unqualified-swap idiom. Understanding classic ADL remains important because it appears throughout the standard library and existing C++ codebases.

Common Mistakes

  • Calling only std::swap in generic code: this can miss a type-specific non-member overload discovered through ADL.
  • Implementing custom swap with assignment: this can add unnecessary work or recurse back into copy-and-swap assignment.
  • Forgetting a member: every state-bearing member must participate in the exchange unless omission is intentional.
  • Claiming noexcept incorrectly: a throwing operation inside a non-throwing function can terminate the program.
  • Putting the overload in an unrelated namespace: ADL will not find it simply because the function has the right name.
  • Over-customizing trivial types: if ordinary std::swap is already correct and efficient, a custom overload may add no value.

Practice Lab

  1. Create a class that owns a std::unique_ptr<int[]> and a size value.
  2. Write a member swap that exchanges both members.
  3. Add a non-member swap in the same namespace.
  4. Write a small generic function using using std::swap; swap(a, b);.
  5. Verify that your custom overload is selected for your class.
  6. Use static_assert(std::is_swappable_v<YourType>);.
  7. If all member swaps are non-throwing, verify std::is_nothrow_swappable_v<YourType>.
  8. Use the custom swap inside a copy-and-swap assignment operator and test self-assignment.

Knowledge Check

  1. Why can a custom swap be faster than generic whole-object swapping?
  2. Where should a non-member swap normally be declared for a user-defined type?
  3. Why does generic code often write using std::swap; before calling swap(a, b)?
  4. What feature lets the compiler search the namespace associated with the argument type?
  5. Why is noexcept valuable for swap?
  6. What happens if an exception escapes a function declared noexcept?
  7. Which C++17 trait reports whether a type can be swapped without throwing?
  8. What modern C++20 customization point can also perform generic swapping?

Answer Guide

  1. It can exchange resource handles and metadata directly rather than reconstructing or copying the resource.
  2. In the same namespace as the user-defined type.
  3. It keeps the standard fallback available while allowing an ADL-found type-specific overload to win.
  4. Argument-dependent lookup, also called Koenig lookup.
  5. It allows swap to serve as a reliable non-throwing commit operation and enables stronger generic guarantees.
  6. The program calls std::terminate.
  7. std::is_nothrow_swappable_v<T>.
  8. std::ranges::swap.

Key Takeaway

Custom swap is a small function with a large role in modern C++ design. Put the non-member overload beside the type, exchange members directly, use using std::swap; swap(...) when writing generic code, and declare noexcept only when the member exchanges truly cannot throw.

References

Primary references: cppreference — std::swap, cppreference — std::ranges::swap, and cppreference — Argument-dependent lookup.


Advertisement

BitcoinVersus.Tech Editor’s Note:

We volunteer daily to ensure the credibility of the information on this platform is Verifiably True. If you would like to support our independent technical education work, please donate Bitcoin here: 3C9o19EH5HSiwEPyCTmEKzxhNCbo2X6TTb

BitcoinVersus.tech is not a financial advisor. This lesson is for informational and educational purposes.

One response to “OSC++.026: Custom swap and ADL — using std::swap, noexcept, and Swappable Types”

  1. […] lesson follows OSC++.026: Custom swap and ADL, OSC++.025: Copy-and-Swap and Exception-Safe Assignment, and OSC++.024: Rule of Five and Rule of […]

    Like

Leave a Reply