K2A-B1-5
C++ Reference, Copy, Move and Forwarding
Created 2018-09-27Updated 2026-02-28programming-language-stack / c_or_cpp / language
- Description: A note on the C++ references vs pointers, value categories, copy and move semantics, the special member functions, and perfect forwarding
- My Notion Note ID: K2A-B1-5
- Created: 2018-09-27
- Updated: 2026-02-28
- License: Reuse is very welcome. Please credit Yu Zhang and link back to the original on yuzhang.io
Table of Contents
- 1. Reference vs Pointer
- 2. Value Categories: Lvalues and Rvalues
- 3. Lvalue Reference (
&) vs Rvalue Reference (&&) - 4. Copy Semantics
- 5. Move Semantics
- 6. The Special Member Functions
- 7. Reference Collapsing Rules
- 8. Forwarding References (Universal References)
- 9.
std::forwardand Perfect Forwarding - 10.
std::movevsstd::forward - 11.
std::reference_wrapperand Container Reference Types - 12. RVO and Copy Elision
1. Reference vs Pointer
| Aspect | Reference | Pointer |
|---|---|---|
| What it is | An alias (another name) for an existing object | An object that stores a memory address |
| Initialization | Must be initialized at declaration | Can be uninitialized (but shouldn't be) |
| Reassignment | Cannot be rebound to another object | Can point to different objects |
| Null | Cannot be null | Can be null |
| Memory | Does not create a new object | Is itself an object (occupies memory) |
int x = 10;
int& ref = x; // ref is an alias for x
ref = 20; // x is now 20
int* ptr = &x; // ptr stores the address of x
*ptr = 30; // x is now 30
ptr = nullptr; // valid; ref = nullptr would not compile
2. Value Categories: Lvalues and Rvalues
- Essential for copy, move, forwarding.
- Lvalue — expression with persistent identity (has a name, addressable). E.g., a variable.
- Rvalue — temporary without persistent identity. E.g., literal, function return, result of
std::move.
int x = 42; // x is an lvalue
int y = x + 1; // (x + 1) is an rvalue
std::string s = std::string("hello"); // std::string("hello") is an rvalue
- Formal C++11 taxonomy more nuanced — xvalues, prvalues, glvalues — but lvalue/rvalue is enough for everyday code.
3. Lvalue Reference (&) vs Rvalue Reference (&&)
- C++11 added
T&&rvalue refs alongsideT&lvalue refs. Difference = what they bind to:
T&— lvalues only ("things with a name").T&&— rvalues only ("temporaries about to disappear").const T&— both lvalues and rvalues. Whyconst T&accepts any argument.
void f(int& x) { /* called for lvalues */ }
void f(int&& x) { /* called for rvalues */ }
void g(const int& x) { /* called for both */ }
int a = 1;
f(a); // int& (a is an lvalue)
f(std::move(a)); // int&& (std::move casts to rvalue)
f(42); // int&& (42 is a temporary)
- Foundation of move semantics — write 2 overloads: lvalue (copies) + rvalue (moves).
4. Copy Semantics
- Copy = independent duplicate. After copy, both objects exist independently — modifying one doesn't affect the other.
4.1 Copy Constructor
- Takes
const T&, produces newT. Called when initializing new object from existing.
class Buffer {
public:
Buffer(const Buffer& other); // copy constructor
};
Buffer a;
Buffer b = a; // copy ctor: a is an lvalue
Buffer c(a); // copy ctor (direct initialization)
Buffer d{a}; // copy ctor (uniform initialization)
4.2 Copy Assignment
- Overwrites already-constructed object with a copy.
class Buffer {
public:
Buffer& operator=(const Buffer& other); // copy assignment
};
Buffer a, b;
a = b; // copy assignment (NOT initialization)
Conventional impl:
- Guard against self-assignment (
if (this == &other) return *this;). - Release existing resources.
- Allocate fresh, copy
other's contents. - Return
*thisby reference.
- Copy-and-swap idiom — combines copy ctor +
swapmethod. Automatic strong exception safety.
4.3 Default Behavior and When It Goes Wrong
- No user-written copy ctor/assign -> compiler synthesizes member-wise copy (each member copied via its own copy ops).
- Correct when every member self-copy-correct (
string,vector,shared_ptr, primitives). - Wrong when a member is a raw pointer to an owned resource:
class BadBuffer {
int* data_; // owned, but compiler will just copy the pointer
public:
BadBuffer() : data_(new int[10]) {}
~BadBuffer() { delete[] data_; }
// No copy ctor written — compiler generates one that copies the pointer
};
BadBuffer a;
BadBuffer b = a; // both a.data_ and b.data_ point to the same array.
// When both go out of scope: double-delete -> crash.
- Central reason for Rule of Five: own a non-RAII resource -> write all 5 special members consistently.
5. Move Semantics
- Move (C++11) = transfer resources without copying. Big perf win for types owning heap memory, file handles, etc.
5.1 Move Constructor and Move Assignment
- Move ctor — steals resources from rvalue source; leaves source in valid-but-unspecified state.
- Move assignment — same on existing object.
class Buffer {
public:
Buffer(Buffer&& other) noexcept; // move constructor
Buffer& operator=(Buffer&& other) noexcept; // move assignment
};
Buffer make_buffer();
Buffer a = make_buffer(); // move ctor (return value is rvalue)
Buffer b;
b = make_buffer(); // move assignment
- Conventional impl: steal source's pointer/handle, reset source to empty. Typically O(1) — much faster than copy.
- Why
noexcept: containers likestd::vectoruse move (vs copy) on realloc only when moves arenoexcept. Forgetting silently degrades perf onpush_backrealloc.
5.2 std::move
std::movedoesn't actually move. Unconditional cast to rvalue ref (T&&) — signals object is safe to move from.
#include <utility>
#include <string>
#include <iostream>
std::string a = "hello";
std::string b = std::move(a); // a's contents are transferred to b
// a is now in a valid but unspecified state (typically empty)
std::cout << b << std::endl; // "hello"
std::cout << a << std::endl; // "" (typically)
- Key rule: after move, only do ops that don't depend on object value — assign, destroy, check if empty.
5.3 Move-Only Types
- Some types only make sense to move, not copy:
unique_ptr,thread,future, file handles. - Declare copy ctor + copy assign as
=delete:
class FileHandle {
public:
FileHandle(const FileHandle&) = delete; // no copy
FileHandle& operator=(const FileHandle&) = delete; // no copy assign
FileHandle(FileHandle&&) noexcept; // OK to move
FileHandle& operator=(FileHandle&&) noexcept; // OK to move
};
- Canonical way to express unique ownership in modern C++.
6. The Special Member Functions
- Every class has 6 special members. Compiler generates implicitly when needed. Can
=default,=delete, or hand-write.
| Function | Signature | When called |
|---|---|---|
| Default constructor | T() |
T x; |
| Destructor | ~T() |
end of x's lifetime |
| Copy constructor | T(const T&) |
T y = x; (where x is an lvalue) |
| Copy assignment | T& operator=(const T&) |
y = x; (where x is an lvalue) |
| Move constructor | T(T&&) |
T y = std::move(x); |
| Move assignment | T& operator=(T&&) |
y = std::move(x); |
- Implicit deletion: declaring any of the 5 non-default special members can suppress others.
- Custom dtor suppresses implicit move ctor + move assignment — class falls back to copies even on rvalues. Why Rule of Five exists.
- When to write all 5 (Rule of Five) vs let compiler generate (Rule of Zero), with worked Buffer example — see K2A-B1-6 § 5.
7. Reference Collapsing Rules
- References-to-references (via templates or typedefs) collapse:
| Form | Result |
|---|---|
T& & |
T& |
T& && |
T& |
T&& & |
T& |
T&& && |
T&& |
- Mnemonic: lvalue ref anywhere -> result is lvalue ref. Only
&& &&-> rvalue ref. - This is the mechanism making perfect forwarding work — when
Tdeduces toint&,T&&becomesint& &&-> collapses toint&.
8. Forwarding References (Universal References)
- Forwarding reference =
T&&whereTis a deduced template parameter. Binds to both lvalues + rvalues, deducingTaccordingly:
- Lvalue of type
intpassed ->T = int&,T&&collapses toint&. - Rvalue of type
intpassed ->T = int,T&&staysint&&.
template <typename T>
void f(T&& arg); // arg is a forwarding reference (not an rvalue reference)
int x = 42;
f(x); // T = int&, arg type = int& (lvalue)
f(42); // T = int, arg type = int&& (rvalue)
f(std::move(x)); // T = int, arg type = int&& (rvalue)
- Note:
auto&&is also a forwarding reference, same deduction rules.
9. std::forward and Perfect Forwarding
std::forward— conditional cast to rvalue ref, preserving original value category.- Used in template fns to pass args through exactly as received.
#include <utility>
#include <iostream>
void process(int& x) { std::cout << "lvalue: " << x << std::endl; }
void process(int&& x) { std::cout << "rvalue: " << x << std::endl; }
template <typename T>
void wrapper(T&& arg) {
// Without forward: arg is always an lvalue (it has a name)
// With forward: preserves the original value category
process(std::forward<T>(arg));
}
int main() {
int x = 42;
wrapper(x); // Calls process(int&) -- lvalue preserved
wrapper(42); // Calls process(int&&) -- rvalue preserved
wrapper(std::move(x)); // Calls process(int&&) -- rvalue preserved
return 0;
}
9.1 How std::forward Works
T = int&(lvalue passed) ->std::forward<int&>(arg)returnsint&(lvalue).T = int(rvalue passed) ->std::forward<int>(arg)returnsint&&(rvalue).
- Exactly what reference collapsing gives -> forwarding is "perfect."
9.2 Practical Use: Factory Function
- Common: perfect forwarding in factories/ctors to avoid unnecessary copies:
#include <memory>
#include <utility>
template <typename T, typename... Args>
std::unique_ptr<T> make(Args&&... args) {
return std::unique_ptr<T>(new T(std::forward<Args>(args)...));
}
// Essentially what std::make_unique does
10. std::move vs std::forward
| Aspect | std::move |
std::forward |
|---|---|---|
| Purpose | Unconditionally cast to rvalue | Conditionally preserve value category |
| Use when | You know you want to move | Forwarding template arguments |
| Takes | Any expression | A forwarding reference argument |
| Result | Always an rvalue reference | Lvalue or rvalue, depending on original |
11. std::reference_wrapper and Container Reference Types
- STL containers store values, not refs —
std::vector<int&>illegal. - For refs in containers, or refs through decaying APIs (
std::thread,std::bind,std::make_pair) -> usestd::reference_wrapper<T>.
#include <functional>
#include <vector>
int a = 1, b = 2, c = 3;
std::vector<std::reference_wrapper<int>> v{a, b, c};
v[0].get() = 10; // modifies a
std::cout << a; // 10
// Helpers: std::ref(x), std::cref(x)
std::vector<std::reference_wrapper<int>> w{std::ref(a), std::ref(b)};
auto cw = std::cref(a); // reference_wrapper<const int>
reference_wrapper<T>implicitly convertible toT&-> most range-for + algorithm uses just work. Use.get()for explicit reference.
Why pass-through APIs need it
void worker(int& x) { x = 42; }
int n = 0;
std::thread t1(worker, n); // BUG: n is COPIED (decay), worker writes to the copy
std::thread t2(worker, std::ref(n)); // OK: passes by reference
std::thread,std::bind,std::make_tupleetc. decay args by default -> refs lost unless wrapped instd::ref/std::cref.
Container const_reference typedef
- Each STL container has
value_type+ matchingreference/const_reference:
std::vector<int>::reference r1 = v[0]; // int&
std::vector<int>::const_reference r2 = v[0]; // const int&
std::vector<bool>::reference rb = vb[0]; // proxy type, NOT bool&
vector<bool>outlier —referenceis a proxy, not realbool&(packs bits). Be careful withauto:
std::vector<bool> v{true, false};
auto x = v[0]; // proxy reference, NOT bool!
v.push_back(false); // vector reallocates, x dangles
12. RVO and Copy Elision
- Returning local by value used to be expensive (copy into caller's slot).
- Modern compilers elide this copy (and the corresponding move) under specific rules.
12.1 Named RVO and Unnamed RVO
Buffer make_buffer_named() {
Buffer b; // local, "named" return
b.fill(0);
return b; // NRVO: constructed directly in the caller's slot
}
Buffer make_buffer_unnamed() {
return Buffer{}; // unnamed return — guaranteed elision (C++17+)
}
- Unnamed RVO (returning temporary) — mandatory since C++17. Temporary constructed directly into caller's storage.
- Named RVO (returning local) — allowed, not required. Modern compilers do it for trivial cases.
12.2 What Defeats Elision
Buffer bad() {
Buffer b;
if (cond) return b;
Buffer c;
return c; // NRVO impossible — two return points with different locals
}
Buffer also_bad(Buffer b) {
return b; // NRVO impossible — b is a parameter, lives in caller's frame
// BUT: implicit move applies; the parameter is moved into the return slot
}
Things that prevent NRVO:
- Multiple return locals — different vars on different paths.
- Returning a function parameter — lives in caller's frame.
- Returning a different local than the one named on some paths.
- When NRVO impossible -> compiler falls back to implicit move of local into return slot (much faster than copy). Mandated even when local is non-
const.
12.3 Practical Implications
- Return by value is cheap — don't write output params / pointer outputs to avoid copies. Return by value, let RVO/move handle it.
return std::move(local);usually wrong — defeats NRVO, forces a (cheaper) move where (free) elision could have happened.- Don't rely on NRVO for correctness — only on
Buffer{...}(mandatory unnamed elision) for guaranteed semantics.