Using std::exchange in C++ Move Assignment
In modern C++, std::exchange is a utility function that
replaces the value of an object with a new one and returns the original
value. During move assignment operations, its primary function is to
transfer ownership of a resource while simultaneously resetting the
moved-from object to a safe, default state in a single, expressive step.
This eliminates boilerplate code, reduces the risk of leaving dangling
pointers or unreset handles, and ensures that the moved-from object
remains in a valid state.
How std::exchange Works
Defined in the <utility> header since C++14, the
signature of std::exchange is conceptually:
template<class T, class U = T>
T exchange(T& obj, U&& new_val);It assigns new_val to obj and returns the
previous value of obj.
The Problem in Move Assignment
When manually implementing a move assignment operator for a class managing resources (such as raw pointers, file descriptors, or socket handles), the implementation must achieve three goals:
- Release the current object's existing resources.
- Steal the resource from the source object (
other). - Reset the source object's member to an empty or default state (e.g.,
nullptror-1) so that its destructor does not double-free the resource.
A naive or verbose implementation often looks like this:
class Buffer {
int* data;
size_t size;
public:
Buffer& operator=(Buffer&& other) noexcept {
if (this != &other) {
delete[] data; // 1. Clean up existing resource
data = other.data; // 2. Steal resource
size = other.size;
other.data = nullptr; // 3. Reset source to valid state
other.size = 0;
}
return *this;
}
};This manual assignment followed by explicit nullification can lead to bugs if a developer forgets to reset a field or introduces order-of-operation errors.
The Solution with
std::exchange
std::exchange combines the extraction of the old
resource and the assignment of the reset value into an atomic conceptual
step:
Buffer& operator=(Buffer&& other) noexcept {
if (this != &other) {
delete[] data;
data = std::exchange(other.data, nullptr);
size = std::exchange(other.size, 0);
}
return *this;
}In this pattern:
other.datais immediately overwritten withnullptr.- The previous pointer held by
other.datais returned and assigned todata. other.sizeis reset to0, and its previous value is assigned tosize.
Key Benefits
- Guaranteed Reset: It ensures the source object is
put into a valid, neutral state at the exact moment the resource is
moved, preventing double-deletion when
othereventually goes out of scope. - Conciseness and Readability: It eliminates the need for separate lines to nullify pointers or zero out counters, making move constructors and move assignment operators shorter and less error-prone.
- Safe Resource Swapping: In scenarios where the
existing resource is deleted after acquiring the new one,
std::exchangecan safely extract the incoming pointer without creating dangling state windows:
delete[] std::exchange(data, std::exchange(other.data, nullptr));By ensuring that moved-from members are reset in a clean and uniform
manner, std::exchange serves as the standard idiom for
managing member-wise resource transfers in custom C++ move
semantics.