What Does std::move Do in C++ Value Semantics?

In C++, std::move serves as an unconditional cast that converts an expression into an rvalue reference, signaling to the compiler that the associated resource can be safely transferred rather than duplicated. This mechanism underpins move semantics by enabling move constructors and move assignment operators to reassign ownership of internal heap allocations, file handles, or system buffers. Crucially, std::move performs no runtime operation, copies no bytes, and leaves the moved-from object in a valid but unspecified state, drastically reducing heap churn and improving performance across modern C++ applications.

Value Semantics and Rvalue References

C++ relies heavily on value semantics, meaning variables behave as independent entities whose copies are completely distinct from their originals. Historically, assigning or passing a heavy object—such as a std::vector or std::string—by value mandated deep copies. This design guaranteed safety and isolation at the expense of performance when temporary objects were repeatedly allocated and destroyed.

C++11 introduced rvalue references (T&&) to distinguish temporary, expiring expressions (prvalues and xvalues) from persistent, named variables (lvalues):

Because named variables are always evaluated as lvalues—even if their declared type is an rvalue reference—the compiler will select copy operations by default to prevent accidental data loss. To invoke a move operation on an existing named object, the programmer must explicitly convert it into an rvalue.

The True Role of std::move

Despite its name, std::move does not move data, free memory, or execute CPU instructions at runtime. It is purely a compile-time cast. Under the hood, std::move removes reference qualifiers and applies a static cast to an rvalue reference type:

template 
constexpr std::remove_reference_t&& move(T&& arg) noexcept {
    return static_cast&&>(arg);
}

By casting an lvalue to an rvalue (specifically an xvalue, or "expiring value"), std::move enables overload resolution to select move-aware functions:

std::vector source = {1, 2, 3, 4, 5};

// Invokes the move constructor instead of the copy constructor
std::vector destination = std::move(source);

In this transaction, destination simply steals the pointer to the underlying dynamic array allocated by source, leaving source with a null pointer or empty capacity. The operation completes in constant time (\(O(1)\)) rather than linear time (\(O(N)\)), bypassing memory allocations.

State of the Moved-From Object

Using std::move leaves the source object in a "valid but unspecified" state according to the ISO C++ standard:

  1. Destructible: The object's destructor must be able to run without causing undefined behavior (such as double frees).
  2. Assignable: The object can be assigned a new value to reuse its storage.
  3. Unspecified values: Unless explicitly guaranteed by the class contract (such as std::unique_ptr being reset to nullptr), code must not assume the internal values of the moved-from object persist.

Reading from a moved-from object without reinitializing it is an anti-pattern that often leads to subtle bugs or logic errors.

Common Pitfalls and Best Practices

To employ std::move effectively within value-semantic architectures, keep these core guidelines in mind: