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:

  1. Release the current object's existing resources.
  2. Steal the resource from the source object (other).
  3. Reset the source object's member to an empty or default state (e.g., nullptr or -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.data is immediately overwritten with nullptr.
  • The previous pointer held by other.data is returned and assigned to data.
  • other.size is reset to 0, and its previous value is assigned to size.

Key Benefits

  1. 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 other eventually goes out of scope.
  2. 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.
  3. Safe Resource Swapping: In scenarios where the existing resource is deleted after acquiring the new one, std::exchange can 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.