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::swapis not always the best operation for a user-defined type. - Write a member
swapthat exchanges resources directly. - Write a non-member
swapin 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
noexceptwhen 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.

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.
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.
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::swapin 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
noexceptincorrectly: 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::swapis already correct and efficient, a custom overload may add no value.
Practice Lab
- Create a class that owns a
std::unique_ptr<int[]>and a size value. - Write a member
swapthat exchanges both members. - Add a non-member
swapin the same namespace. - Write a small generic function using
using std::swap; swap(a, b);. - Verify that your custom overload is selected for your class.
- Use
static_assert(std::is_swappable_v<YourType>);. - If all member swaps are non-throwing, verify
std::is_nothrow_swappable_v<YourType>. - Use the custom swap inside a copy-and-swap assignment operator and test self-assignment.
Knowledge Check
- Why can a custom swap be faster than generic whole-object swapping?
- Where should a non-member swap normally be declared for a user-defined type?
- Why does generic code often write
using std::swap;before callingswap(a, b)? - What feature lets the compiler search the namespace associated with the argument type?
- Why is
noexceptvaluable for swap? - What happens if an exception escapes a function declared
noexcept? - Which C++17 trait reports whether a type can be swapped without throwing?
- What modern C++20 customization point can also perform generic swapping?
Answer Guide
- It can exchange resource handles and metadata directly rather than reconstructing or copying the resource.
- In the same namespace as the user-defined type.
- It keeps the standard fallback available while allowing an ADL-found type-specific overload to win.
- Argument-dependent lookup, also called Koenig lookup.
- It allows swap to serve as a reliable non-throwing commit operation and enables stronger generic guarantees.
- The program calls
std::terminate. std::is_nothrow_swappable_v<T>.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.

Leave a Reply