Move Semantics and Value Categories
|
This section documents C++23 (ISO/IEC 14882:2024), as published by ISO/IEC JTC1/SC22/WG21 (wg21), verified against the freely available working draft N5046 (eel.is/c++draft) and cppreference.com. This content was generated with the assistance of AI and should be verified against the working draft and cppreference.com before being relied on in production. This section’s bibliography lists the reference material consulted while preparing these pages. |
Value categories (lvalue/prvalue/xvalue) were introduced in Operators and Expressions; this page covers what they make possible: transferring ownership of a resource instead of copying it.
Rvalue References and std::move
An rvalue reference (T&&) binds only to rvalues (temporaries, or anything explicitly cast to one), which
is exactly the signal a type needs to safely "steal" from its argument instead of copying:
#include <utility>
#include <vector>
class Buffer {
public:
explicit Buffer(std::size_t size) : size_(size), data_(new int[size]) {}
~Buffer() { delete[] data_; }
Buffer(const Buffer& other) // copy: allocates a new buffer, copies elements
: size_(other.size_), data_(new int[other.size_]) {
std::copy(other.data_, other.data_ + size_, data_);
}
Buffer(Buffer&& other) noexcept // move: steals the pointer, leaves other empty
: size_(other.size_), data_(other.data_) {
other.size_ = 0;
other.data_ = nullptr;
}
private:
std::size_t size_;
int* data_;
};
int main() {
Buffer a(1000);
Buffer b = std::move(a); // a's buffer is now owned by b; a is left in a valid-but-unspecified,
// "moved-from" state -- safe to destroy or reassign, but not to read
}
std::move does not itself move anything — it is purely a static_cast<T&&>, i.e. it casts its argument to
an xvalue so overload resolution picks the move overload instead of the copy overload.
std::forward and Perfect Forwarding
In a template, a forwarding reference (T&& where T is deduced) can bind to either an lvalue or an
rvalue; std::forward<T> preserves which one it originally was when passing it along, so a generic wrapper
doesn’t force an unwanted copy:
#include <utility>
#include <memory>
template <typename T, typename... Args>
std::unique_ptr<T> makeUniqueLike(Args&&... args) { // Args&& here are forwarding references
return std::make_unique<T>(std::forward<Args>(args)...); // forwards each argument as it originally was
}
struct Point { Point(int, int) {} };
// makeUniqueLike<Point>(1, 2) forwards two rvalues (int literals) without any unnecessary copy
Do not confuse T&& in a template with deduced T (a forwarding reference) with T&& where T is a concrete,
non-deduced type (an ordinary rvalue reference, as in Buffer(Buffer&& other) above) — the syntax looks
identical but the binding rules differ.
Copy Elision and Guaranteed Elision
Since C++17, returning a temporary by value from a function is guaranteed to construct directly in the caller’s storage — no copy or move constructor is even required to exist, let alone called:
Buffer makeBuffer() {
return Buffer(1000); // guaranteed elision (C++17): constructed directly into the caller's variable
}
int main() {
Buffer buf = makeBuffer(); // no copy, no move -- Buffer is built in buf's storage directly
}
Named Return Value Optimization (NRVO) — eliding the copy/move of a named local returned from a function — is still only an optional (though near-universally applied) optimization, not guaranteed by the standard:
Buffer makeBufferNamed() {
Buffer local(1000);
return local; // NRVO likely applies (not guaranteed) -- and even if it didn't, "local" is an
} // lvalue that the compiler still implicitly treats as movable-from here
noexcept Moves
std::vector (and other containers) only move elements during reallocation if the move constructor is
noexcept — otherwise it falls back to copying, to preserve the strong exception-safety guarantee (a
reallocation that fails partway through must not leave the original data corrupted):
class SafeToMove {
public:
SafeToMove(SafeToMove&&) noexcept = default; // vector will move these on reallocation
SafeToMove(const SafeToMove&) = default;
};
class RiskyToMove {
public:
RiskyToMove(RiskyToMove&&) = default; // NOT noexcept -- vector will copy these instead,
RiskyToMove(const RiskyToMove&) = default; // even though a move constructor exists
};
Always mark a move constructor/assignment noexcept when it genuinely cannot throw (which is nearly always,
since a move typically just swaps a few pointers/scalars).
Returning by Value
Given guaranteed elision, returning a large object by value is now the idiomatic, efficient choice in most
cases — there is rarely a reason to reach for an output parameter (void compute(Result& out)) purely for
performance:
#include <vector>
std::vector<int> computeSquares(int n) {
std::vector<int> result;
result.reserve(n);
for (int i = 0; i < n; ++i) result.push_back(i * i);
return result; // elided or, worst case, moved -- never deep-copied
}
See Also
-
Classes and Objects — the rule of zero/three/five.
-
Templates — forwarding references in generic code.