Why This Matters
A std::vector<std::string> with 1,000,000 long strings owns millions of heap bytes, but the vector object itself is usually three machine words: begin pointer, end pointer, capacity-end pointer. Moving the vector can copy those 24 bytes on a 64-bit target and leave the source empty. Copying it must allocate new storage and copy or construct every string.
C++03 expressed only copying in most interfaces. C++11 added rvalue references so a type can say when it is allowed to steal representation instead of duplicating it. That change is why returning a large std::vector<float> from a function is ordinary C++ rather than an accidental quadratic allocation pattern.
Core Definitions
Value category
An expression has a value category. Since C++11, the main categories are lvalue, xvalue, and prvalue. An lvalue has identity and can be named after the expression, such as v or *p. An xvalue has identity but is treated as expiring, such as std::move(v). A prvalue computes a value without a persistent object identity in the expression, such as 42 or std::vector<int>{1,2,3}.
Rvalue reference
T&& is an rvalue reference type in a non-deduced context. It binds to rvalues, including xvalues and prvalues, and gives a named, mutable reference inside the function. It is distinct from const T&, so overload resolution can select a move constructor or move assignment operator.
Move operation
A move constructor T(T&&) and move assignment operator T& operator=(T&&) transfer resources from a source object into a destination object. The source remains valid and destructible, but its value is usually unspecified unless the type documents more.
Perfect forwarding
Perfect forwarding preserves the value category of a template argument. In a template parameter of the form T&& where T is deduced, the parameter is a forwarding reference. std::forward<T>(x) casts x back to the category with which it arrived.
Value Categories And Overload Selection
The old two-way language, lvalue versus rvalue, was too coarse. C++11 split rvalues because temporary materialization, identity, and stealing are different concerns.
#include <string>
#include <utility>
void f(const std::string&); // accepts lvalues and rvalues, cannot mutate argument
void f(std::string&&); // accepts rvalues, may steal representation
std::string make();
int main() {
std::string a = "abcdef";
f(a); // lvalue, calls f(const std::string&)
f(std::string{}); // prvalue, calls f(std::string&&)
f(make()); // prvalue result, calls f(std::string&&)
f(std::move(a)); // xvalue, calls f(std::string&&)
}
A named rvalue reference is an lvalue expression. This is the rule that prevents accidental double moves inside a function.
void sink(std::string&& s) {
f(s); // s has a name, so expression s is an lvalue
f(std::move(s)); // explicit xvalue conversion
}
std::move is badly named if read as an instruction. It does not move bytes, free storage, call a constructor, or change an object by itself. It is a cast:
template<class T>
constexpr std::remove_reference_t<T>&& move(T&& t) noexcept {
return static_cast<std::remove_reference_t<T>&&>(t);
}
The actual move happens only if overload resolution selects a move constructor, move assignment operator, or another overload that consumes T&&.
Move Constructors As Swapping The Guts
A small vector-like owner exposes the representation cost. Suppose Buffer owns size bytes at data.
#include <algorithm>
#include <cstddef>
#include <cstring>
#include <utility>
struct Buffer {
unsigned char* data = nullptr;
std::size_t size = 0;
Buffer() = default;
explicit Buffer(std::size_t n) : data(new unsigned char[n]), size(n) {}
~Buffer() { delete[] data; }
Buffer(const Buffer& other) : data(new unsigned char[other.size]), size(other.size) {
std::memcpy(data, other.data, size);
}
Buffer& operator=(const Buffer& other) {
if (this == &other) return *this;
Buffer tmp(other);
swap(tmp);
return *this;
}
Buffer(Buffer&& other) noexcept : data(other.data), size(other.size) {
other.data = nullptr;
other.size = 0;
}
Buffer& operator=(Buffer&& other) noexcept {
if (this == &other) return *this;
delete[] data;
data = other.data;
size = other.size;
other.data = nullptr;
other.size = 0;
return *this;
}
void swap(Buffer& other) noexcept {
std::swap(data, other.data);
std::swap(size, other.size);
}
};
On a 64-bit little-endian target, a Buffer object with pointer 0x0000000012345000 and size 16 commonly occupies 16 bytes:
address +0x00: 00 50 34 12 00 00 00 00 data pointer
address +0x08: 10 00 00 00 00 00 00 00 size_t size
A copy of this object allocates a second 16-byte array and copies 16 payload bytes. A move copies the two fields and writes zeros into the source fields:
before move:
src: data=0x0000000012345000 size=16
dst: data=0x0000000000000000 size=0
after move:
src: data=0x0000000000000000 size=0
dst: data=0x0000000012345000 size=16
The destructor now runs once on the heap allocation because only dst.data keeps the pointer. This is the usual shape of move semantics for file descriptors, sockets, heap arrays, mutex guards, and GPU buffers. The representation changes, not the abstract data alone.
noexcept matters for containers. During reallocation, std::vector<T> prefers moving elements only when it can preserve its exception guarantee. If T's move constructor can throw and copying is available, many standard library implementations copy instead.
Rule Of Zero, Three, And Five
The rule of three says that if a class declares one of destructor, copy constructor, or copy assignment operator, it probably needs all three. The reason is ownership. A destructor that frees a pointer implies copying the pointer bit pattern is wrong.
C++11 extends this to the rule of five by adding move constructor and move assignment operator. A type that manually owns a resource often needs all five special member functions.
The rule of zero is the preferred design. Store ownership in library members whose special member functions are already correct.
#include <memory>
#include <vector>
struct ModelWeights {
std::vector<float> dense; // movable and copyable
std::unique_ptr<float[]> scratch; // movable, not copyable
std::size_t scratch_size = 0;
// No destructor, no copy/move declarations.
// The compiler generates or deletes operations from members.
};
The compiler-generated operations follow member behavior. Because std::unique_ptr<float[]> is not copyable, ModelWeights is not copyable. Because it is movable, ModelWeights is movable. This avoids writing pointer code and reduces double-free bugs.
If a type has raw ownership, write the five. If a type only composes standard owners, write none.
Worked Example: std::vector<std::string>::push_back
std::vector<T>::push_back has overloads equivalent to these:
void push_back(const T& value); // copy into new element
void push_back(T&& value); // move into new element
For T = std::string, the difference depends on representation. A short string may sit inside the string object. A long string usually owns heap storage. The standard does not mandate the layout, but a typical long string representation contains a character pointer, a size, and a capacity.
#include <string>
#include <vector>
#include <utility>
int main() {
std::vector<std::string> v;
v.reserve(2);
std::string s(100, 'x');
v.push_back(s); // copies 100 characters into new storage
v.push_back(std::move(s)); // moves string representation when possible
}
A typical long-string trace looks like this:
before second push:
s: ptr=0x0000000040001000 size=100 cap=100
v[0]: ptr=0x0000000040002000 size=100 cap=100
after v.push_back(std::move(s)):
s: ptr=null-or-small-state size=0 cap=SSO
v[1]: ptr=0x0000000040001000 size=100 cap=100
The second push does not make s illegal. It does make code that reads the old text from s wrong. You may assign to it, destroy it, call empty(), or use operations with documented postconditions. You must not assume it still holds 100 copies of 'x'.
If v must reallocate, it also moves or copies existing elements into a new array. With capacity 2, pushing a third string allocates a larger internal array. The vector object may remain three pointers, but its storage changes:
vector object before:
begin=0x7000 end=0x7040 cap=0x7040 // two 32-byte string objects
after third push and reallocation:
begin=0x9000 end=0x9060 cap=0x9080 // three live elements, four slots
old [0x7000,0x7040) destroyed and freed
Move construction of the string elements can turn that reallocation into pointer transfers for long strings. That is why string move constructors are marked noexcept in standard library implementations that can do so.
Perfect Forwarding With std::forward
A wrapper often wants to pass arguments to another constructor without changing lvalues into rvalues or rvalues into lvalues.
#include <memory>
#include <utility>
template<class T, class... Args>
std::unique_ptr<T> make_owned(Args&&... args) {
return std::unique_ptr<T>(new T(std::forward<Args>(args)...));
}
If the caller passes an lvalue std::string name, template deduction sets Args to std::string&, and std::forward<Args>(arg) returns an lvalue. If the caller passes std::string("tmp"), deduction sets Args to std::string, and std::forward<Args>(arg) returns an rvalue.
Using std::move(args)... here would be a bug. It would move from lvalues supplied by the caller.
struct User {
std::string name;
explicit User(std::string n) : name(std::move(n)) {}
};
std::string n = "ada";
auto p = make_owned<User>(n); // n is copied into parameter path, not stolen
Forwarding references occur only with type deduction. In void g(std::string&&), std::string&& is an rvalue reference. In template<class T> void g(T&&), T&& is a forwarding reference.
Copy Elision And Returning Values
Move semantics made return-by-value cheaper. Copy elision then removed even that move in many cases.
#include <vector>
std::vector<int> make_vec() {
std::vector<int> v;
v.push_back(10);
v.push_back(20);
return v; // NRVO candidate
}
std::vector<int> make_temp() {
return std::vector<int>{10, 20}; // guaranteed copy elision since C++17
}
Named return value optimization constructs v directly in the caller's return storage when the compiler applies NRVO. C++17 also guarantees copy elision for several prvalue cases, including returning a prvalue of the function return type.
A useful mental model for make_temp is caller-provided storage:
caller reserves vector object slot:
ret: begin=? end=? cap=?
callee constructs vector<int>{10,20} directly in ret:
ret: begin=0x5000 end=0x5008 cap=0x5008
heap at 0x5000: 0a 00 00 00 14 00 00 00
No temporary vector object needs to be moved from. The heap allocation for the two int elements still exists; copy elision removes object-level transfers, not allocation inside the vector.
The Model
For resource-owning types, the move invariant is small and mechanical:
Here R is the owned resource handle, such as a pointer, file descriptor, or vector buffer. After the operation, exactly one live object owns the original handle.
A second invariant is about expression categories rather than resources:
Even when the declared type is T&&, a named variable must be cast with std::move or std::forward<T> to select an rvalue overload.
Move Preserves Single Ownership For Swap-The-Guts Types
Statement
After moving one object into another, the original handle has exactly one owner, and destroying both objects frees the handle at most once.
Intuition
A move is pointer transfer plus source nulling. The destination receives the only live handle. The source destructor sees an empty handle.
Proof Sketch
Before the move, by assumption, src owns handle h and no other object owns h. The constructor writes dst.h = src.h, so dst now contains h. It then writes src.h = null. Thus exactly dst owns h. Destruction calls delete, close, or the matching release operation only for non-empty handles, so h is released at most once.
Why It Matters
This is the safety argument behind moving std::unique_ptr, vector buffers, and file wrappers. Without source reset, move construction would be a shallow copy followed by double destruction.
Failure Mode
Self-move assignment can break the argument if the operator deletes the destination before reading the source and this == &other. Guard self-move or implement assignment by swapping with a temporary when the type permits it.
Common Confusions
std::move moved my object
std::move(x) only changes the expression category to xvalue. The object changes when some selected operation observes that xvalue and performs a move. This line alone does not change s:
std::string s = "abc";
std::move(s); // cast result is discarded
This line can change s because it calls a move constructor:
std::string t = std::move(s);
A moved-from object is destroyed
A moved-from object is still alive. Its destructor will run. You may assign a new value to it:
std::vector<int> a = {1, 2, 3};
std::vector<int> b = std::move(a);
a = {4, 5}; // valid
The mistake is reading semantic content that the move no longer promises, such as assuming a.size() == 3.
T&& always means rvalue reference
In a deduced template parameter, T&& can bind to lvalues and rvalues. It is a forwarding reference. In a concrete signature such as void f(Buffer&&), it is an rvalue reference and does not bind to lvalue Buffer b without std::move(b).
Exercises
Problem
Given this type on a 64-bit little-endian machine, show the field values after move construction.
struct Block {
char* p;
unsigned long n;
Block(Block&& other) noexcept : p(other.p), n(other.n) {
other.p = nullptr;
other.n = 0;
}
};
Before the move, a.p = 0x0000000000603000 and a.n = 32. Block b(std::move(a)); is executed.
Problem
For the overload set below, write which overload is called by each expression.
void h(const std::string&);
void h(std::string&&);
std::string make();
std::string s = "ml";
h(s);
h(std::move(s));
h(make());
h(std::string("tmp"));
Problem
Fix the forwarding bug.
template<class F, class A>
decltype(auto) call_bad(F&& f, A&& a) {
return std::move(f)(std::move(a));
}
Write a corrected version and state what goes wrong when a is an lvalue std::string.
References
Canonical:
- Bjarne Stroustrup, A Tour of C++ (3rd ed., 2022), ch. 6.2.2 and 7.2, covers resource management, move operations, and containers
- Bjarne Stroustrup, The C++ Programming Language (4th ed., 2013), ch. 17.5 and 18.3, covers special member functions, rvalue references, and move semantics
- Stanley B. Lippman, Josée Lajoie, and Barbara E. Moo, C++ Primer (5th ed., 2012), ch. 13.6, covers moving objects and rvalue references
- Nicolai M. Josuttis, The C++ Standard Library (2nd ed., 2012), ch. 3.3 and 6.10, covers move semantics in library types and containers
- Randal E. Bryant and David R. O'Hallaron, Computer Systems: A Programmer's Perspective (3rd ed., 2016), ch. 3.10 and 9.9, covers machine-level layout and dynamic allocation costs behind object movement
Accessible:
- cppreference, "Value categories" and "std::move", concise reference for exact language rules
- Thomas Becker, "C++ Rvalue References Explained", detailed open tutorial with overload examples
- ISO C++ Core Guidelines, sections C.20, C.21, C.64, C.65, rules for special member functions and moved-from states
Next Topics
- /computationpath/cpp-object-lifetime-and-raii
- /computationpath/templates-and-type-deduction
- /computationpath/vector-memory-layout-and-allocators
- /topics/static-dispatch-and-overload-resolution