C++ Rvalue vs Lvalue: A Practical Complete Guide to Value
이 글의 핵심
Learn C++ lvalues and rvalues: value categories, references, move semantics, and std::move—with examples for overload resolution and fewer unnecessary copies.
Every expression in C++ has two orthogonal properties: a type and a value category. The type tells you what operations are valid; the value category tells you how the object is used in overload resolution, reference binding, and lifetime rules. C++11 unified the old “lvalue vs rvalue” picture into a precise taxonomy: glvalue, prvalue, and xvalue. Mastering that taxonomy is the key to move semantics, rvalue references (&&), std::move, std::forward, and the difference between a fast return-by-value and a subtle pessimization.
This guide is written for working C++ engineers who already write classes and templates and want a single, end-to-end reference. We move from intuitions to the standard terminology, then to references, move operations, the Rule of Five, forwarding references, and production pitfalls (including RVO, const, and noexcept).
A note from the hallway (the part that actually sticks)
I’ve explained lvalues, rvalues, and std::move to more than fifty developers in code reviews, pairing sessions, and late-night debugging threads. The standard definitions are correct, but they rarely land on the first pass. What finally clicks is usually the same story: you are not learning a vocabulary test — you are learning what the compiler is allowed to assume about when an object’s bits can be taken without asking again. Once that lands, overload resolution stops feeling like trivia and move semantics stops feeling like magic.
If you take one thing from this article, take that. Everything else is filling in names for what the compiler already does when it picks T& versus T&&, when it elides a return, and when a template parameter named T quietly becomes T& because of reference collapsing.
Introduction: value categories in C++
Before C++11, “lvalue” and “rvalue” were enough for most teaching. C++11 added rvalue references and move semantics, which require a third category, the xvalue (“eXpiring” value), to describe expressions like std::move(x) that refer to named storage but behave like resources about to be pilfered.
So why should you care if you are not writing language-lawyer blog posts? In day-to-day code, four things keep showing up. Overload resolution picks different functions for T& versus T&& versus pass-by-value. Move constructors and move assignment get selected when the argument is an rvalue (including xvalues), which is how you skip deep copies for big buffers. Templates with T&& in a deduced context become forwarding references, and one missing std::forward is enough to silently turn a fast path into copies. Finally, lifetime extension when binding temporaries to references has narrow rules; breaking them is how you get dangling references that pass code review and fail in production.
Here is a small example you can keep in your pocket:
int x = 10; // x is an lvalue; 10 is a prvalue
int& lref = x; // OK: non-const lvalue reference binds to lvalue
// int& bad = 10; // error: non-const lvalue ref cannot bind to temporary
int&& rref = 10; // OK: rvalue ref binds to prvalue (temporary materialized)
// int&& bad2 = x; // error: rvalue ref cannot bind to lvalue
Lvalue: definition and identity
An lvalue (“locator value”) is a glvalue that is not an xvalue. In practice I tell people: if you are looking at a place — not just a fleeting result — you are probably in lvalue land. Lvalues designate a persistent object (or a function) and they have identity: you can usually take an address, unless the language forbids it (think bit-fields, oddities around register in very old code, and a few other corner cases you will meet in the standard, not in every Tuesday standup).
Where do lvalues show up? Named variables are the easy cases (x, buf). Dereference and subscript usually yield lvalues (*p, p[0]). Members and pointer members are lvalues too (obj.member, ptr->member). Preincrement is a good litmus test: ++x is an lvalue to x, which is why expressions like ++(++x) can compile for built-in integers — the inner ++ yields an lvalue the outer ++ can modify. Function names are a bit of a “sometimes” story — they often decay, but a function designator in the right context is still an lvalue. The one that trips people: write int&& rr = 10; and then use rr in an expression — rr is named, so it is an lvalue even though its type is an rvalue reference. That is the same rule that makes perfect forwarding need std::forward.
int x = 0;
int* p = &x; // & requires an lvalue (glvalue) operand in ordinary cases
int& r = x; // lvalue ref binds to lvalue
int arr[3];
&arr[0]; // lvalue: subscript yields lvalue for built-in array
struct S { int a; } s;
&s.a; // lvalue: member of object
For a quick preview of binding: a non-const lvalue reference T& is picky — lvalues of compatible type only. A const lvalue reference const T& is the polite Swiss Army knife: it can also bind to prvalues, at which point temporary materialization and lifetime rules enter the story.
Rvalue: temporaries and expiring values
Rvalue in the C++11 taxonomy is not a single node on the chart: it is the union of prvalue and xvalue. Colloquially, people still say “rvalue” to mean “thing on the right” or “temporary,” and that intuition works for literals and a lot of expression results, but it breaks the moment you meet xvalues — they may still have storage you can point at, even though the language is trying to treat them as expiring.
A prvalue (“pure rvalue”) is the “just computed it” world: a value that is not necessarily sitting in a durable object yet — a literal, a result of x + y, a std::string{} in the right context — until the compiler materializes a temporary when you bind it or when the rules demand an object in memory.
42; // prvalue: integer literal
x + y; // prvalue: result of built-in + for arithmetic
std::string{}; // prvalue: temporary (until bound or used)
std::string s = "hello"; // "hello" is a prvalue (decays / converts as needed)
An xvalue is the interesting hybrid: an “expiring” glvalue with identity, but the language is happy to move from it because you (or a cast) said the resource is done being semantically “owned” by normal lvalue rules.
std::move(x); // xvalue
static_cast<std::string&&>(s);
Cannot bind a non-const lvalue ref to a prvalue — that is the classic C++03 teaching rule that motivates const T& to extend lifetime and T&& for moves.
Glvalue, prvalue, and xvalue: the C++11 taxonomy
C++ classifies every expression into exactly one of lvalue, xvalue, or prvalue. The two “group names” people mix up are glvalue and rvalue, and the trick is to remember what each one is for.
A glvalue is “generalized lvalue”: it is either an lvalue or an xvalue, and the point is identity — there is a real object (or subobject) you are talking about, not just a transient value mid-air. An rvalue in the modern sense is prvalue or xvalue: that is the family the language uses when it wants to say “this is a good moment to move from,” because the expression is not a long-lived lvalue sitting under a stable name.
Picture a prvalue as a freshly computed result that might not even have a home in memory until the language materializes it; picture an xvalue as “this object still exists, but we are explicitly treating it as about to expire,” which is what std::move manufactures. Put an lvalue and an xvalue together and you have covered all glvalues; put a prvalue and an xvalue together and you have covered all rvalues. If that sounds like overlapping Venn diagrams, it is — and that overlap is exactly why std::move is a cast, not a relocation.
Diagram (conceptual)
expression
│
┌───────┴───────┐
glvalue prvalue
│ │
┌────┴────┐ │
lvalue xvalue │
│ │ │
└────┬───┘ │
rvalue ◄───────────┘
(prvalue ∪ xvalue)
prvalue → glvalue temporary materialization occurs when you need a real object in memory (e.g. binding to a reference, typeid on class prvalue, etc.).
Key insight: std::move does not move. It returns an xvalue referring to the same object as the operand. The move constructor (if any) is selected because overload resolution sees an rvalue of the right type.
&& syntax: rvalue references
A rvalue reference has the form T&& when T is a specific, non-deduced type. It binds to rvalues of type T (with qualification conversions).
void take(int&& x);
int a = 1;
// take(a); // error: a is lvalue
take(1); // OK: prvalue
take(std::move(a)); // OK: xvalue
Reference collapsing (when typedefs, templates, or auto combine references):
T& &,T& &&,T&& &all collapse toT&T&& &&collapses toT&&
This is what makes template<typename T> void f(T&& x) able to become int& or int&& after deduction — the template parameter is not a “rvalue ref to rvalue ref” in the user’s mind; the compiler collapses the effective parameter type.
How rvalue references enable move semantics
Copy is the conservative story: leave the source alone and duplicate resources. Move is the opportunistic one: the language believes the source is not needed in its old form after the operation, at least for the purposes of ownership transfer, which is what rvalues (including the xvalue you get from std::move) are signaling.
Concretely, a move-friendly std::vector, std::string, or std::unique_ptr will steal the pointer or handle out of a T&& argument and leave the old object in a valid but unspecified state — often “empty” — that is still safe to assign to or destroy. Overload resolution will pick the move constructor over the copy when the argument is an rvalue and both exist, which is the whole performance story for big owned buffers.
std::move: cast to rvalue reference
std::move is, in typical implementations, an unconditional cast to an xvalue:
template<typename T>
constexpr std::remove_reference_t<T>&& move(T&& t) noexcept {
return static_cast<std::remove_reference_t<T>&&>(t);
}
Read that implementation twice: std::move names a cast, not a call into std::memmove or a byte-blasting algorithm. If there is no move operation in the end, the cast may still route you into a copy — for example with const arguments or when only copy construction exists — or it may simply fail to compile, depending on what you are initializing or assigning. And after you write std::move on local inside a function, local is still a variable in scope; it just might not mean what you think it means unless you treat it with the usual moved-from discipline (covered in the pitfalls section).
Example
std::string a = "long string";
std::string b = std::move(a);
// a is valid but unspecified; often empty. Safe: assign or destroy.
a.clear();
std::forward: perfect forwarding
In a template, parameters are lvalues in the function body even if the caller passed a temporary — because the parameter has a name. Perfect forwarding preserves the value category and const-ness of the original argument for the next call.
template<typename T>
void wrapper(T&& arg) {
// arg is always lvalue in the body
// process(arg); // would always see lvalue
process(std::forward<T>(arg)); // lvalue or xvalue, as from caller
}
Pattern: T&& with T deduced from a forwarding reference; pair with std::forward<T>(arg).
In templates I contrast them like this: std::move(arg) always shoves the argument into rvalue-land, which is wrong for forwarding if the caller handed you an lvalue and the next function still needs to copy. std::forward<T>(arg) is the conditional operation — lvalues stay lvalues, rvalues stay rvalues — which is what “perfect” forwarding is trying to preserve.
Move constructor: implementation
A move constructor has the form T(T&&).
class Buffer {
int* data = nullptr;
size_t size = 0;
public:
explicit Buffer(size_t s) : size(s) { data = new int[size](); }
~Buffer() { delete[] data; }
Buffer(const Buffer& other) : size(other.size) {
data = new int[size];
std::copy(other.data, other.data + size, data);
}
Buffer(Buffer&& other) noexcept
: data(std::exchange(other.data, nullptr))
, size(std::exchange(other.size, 0)) {}
// deleted copy assign / move assign for brevity in a tutorial snippet
};
What I nag people about in move constructors
If the move can be noexcept, say so. std::vector reallocation is the classic reason: the implementation may refuse to move elements if moving might throw, and you will watch your code fall back to copies and wonder why. After a move, null or reset pointer members so destructors do not double-free. Self-assignment belongs in assignment operators; move constructors rarely need that branch.
Move assignment: implementation
Buffer& operator=(Buffer&& other) noexcept {
if (this == &other) { return *this; }
delete[] data;
data = std::exchange(other.data, nullptr);
size = std::exchange(other.size, 0);
return *this;
}
Often, implementers use copy-and-swap to unify copy/move assignment with strong exception safety; for low-level types, manual move assignment as above is common.
Rule of Five: destructor, copy, move, copy assign, move assign
If your class manages a resource (raw pointer, FILE*, handle), you usually need the Rule of Five:
- Destructor — release
- Copy constructor — duplicate
- Copy assignment — destroy old, duplicate
- Move constructor — pilfer, leave source valid
- Move assignment — pilfer, destroy old resource first
Omitting move operations can leave performance on the table; = default is appropriate when the implicitly generated special members do the right thing (often when members are all RAII types).
Rule of Zero (prefer): compose std::vector, std::string, std::unique_ptr and let the compiler generate the right pattern.
Universal (forwarding) references: T&& in templates
Scott Meyers’ term “universal reference” refers to a deduced T&& in a template or auto&& in some contexts, where the deduced T and reference collapsing make T&& act as either an lvalue reference or an rvalue reference to the real argument type.
template<typename T>
void foo(T&& x);
int a = 0;
const int c = 0;
foo(a); // T = int& → T&& is int& && → int&
foo(1); // T = int → T&& is int&&
foo(c); // T = const int& → T&& is const int& (collapses)
Not a forwarding reference: void bar(std::string&& s) with T = std::string in a non-deduced way — that is a normal rvalue ref parameter (only rvalues).
Lifetime extension: const& and temporaries
A const lvalue reference to type T can bind to a prvalue. The temporary’s lifetime is extended to the lifetime of the reference, if the reference is a block-scope variable in the same scope (rules are subtle for subobjects, static, etc.; see the standard and cppreference.
const std::string& cr = make_string(); // often OK: extends temporary
// cr valid until end of full block scope
Non-const lvalue reference cannot bind to a temporary. Rvalue ref to T&& with a temporary is similar extension behavior for the temporary object, but the reference kind matters for what bindings are allowed.
Dangling happens when a reference or pointer outlives the object — e.g. returning const T& to a function-local temporary.
Return value: RVO, NRVO, and value category
Return by value from a function often does not create an intermediate std::string in the caller’s stack in optimized builds — (N)RVO elides the copy/move. The C++17 guaranteed copy elision in specific return cases further reduces reliance on a move.
std::string make() {
std::string s = "data";
return s; // often NRVO: construct s directly in caller’s output slot
// return std::move(s); // often BAD: can inhibit NRVO; prefer plain return
}
return std::move(local) is usually discouraged for a local auto or named variable: it names an xvalue, which can block copy elision in some conditions and only guarantee a move — sometimes worse than elision + zero copy.
prvalue return (e.g. return std::string("x"); or C++17 guaranteed cases) is different: materialization and elision follow standard rules.
Exception: moving members or out-parameters when returning, or from std::optional / variants — some style guides allow explicit std::move out of a tuple member; the situation is more nuanced. Default guidance: plain return for locals.
std::move pitfalls: common mistakes
1) Use after move
Moved-from types are in a valid, unspecified state. For std::vector or std::string, reusing without reset may be safe for some calls but is poor style; for other types, preconditions can fail.
std::vector<int> a = {1,2,3};
std::vector<int> b = std::move(a);
a.clear();
a.push_back(4); // OK after clear for vector
2) const and move
const std::string s = "hi";
// std::string t = std::move(s); // copies: cannot steal from const
3) std::move and return of local
Already discussed: can block RVO / elision; prefer return s;.
4) Moving from subexpressions in wrong order
Evaluation order and moves in initializer lists: minimize clever moves; clarity and NRVO beat micro-optimizations.
5) Move in a hot loop without profiling
Measure; sometimes Small String Optimization (SSO) means moving strings is irrelevant.
std::move_if_noexcept: exception safety in containers
When a container (e.g. std::vector::resize) must provide the strong exception guarantee, it will not use a throwing move. If the move is not noexcept and a copy exists, the implementation may copy lvalues in reallocation to roll back on exception.
std::move_if_noexcept(x) is effectively “move if the move is noexcept, else copy (if available).”
template<typename T>
constexpr typename std::conditional<
!std::is_nothrow_move_constructible_v<T> && std::is_copy_constructible_v<T>,
const T&,
T&&
>::type move_if_noexcept(T& t) noexcept;
Takeaway: mark move constructors noexcept when correct — it directly affects vector reallocation and similar code.
Value category tests: decltype and traits
decltype
decltype(x)wherexis an id-expression of variable: type of the entity, includes references.decltype((x))— extra parens make it an expression; lvaluexyieldsT&for many cases.
int a = 0;
int&& r = 5;
decltype(a) // int
decltype((a)) // int& — common gotcha
decltype(r) // int&&
decltype((r)) // int& — r is lvalue as expression
std::is_lvalue_reference / is_rvalue_reference / is_reference
Test types of deduced T in templates, not the value category of an arbitrary expression (use separate techniques or libraries for that).
Conceptual check
template<typename T>
void print() {
if constexpr (std::is_lvalue_reference_v<T>) {
// deduced lvalue in forwarding context
}
}
Real examples: std::vector and std::unique_ptr internals (conceptual)
std::vector
Think of reallocation as a small logistics problem: the implementation allocates a new buffer, then moves elements if the move is noexcept (otherwise it may copy to preserve the strong exception guarantee), then destroys the old storage. In real services, a throwing move constructor is how you accidentally turn a “fast vector growth” into a copy fest — the move ctor really does need that noexcept when the type allows it. On push_back, a temporary like vec.push_back(std::string("a")) is a playground for emplace-versus-move overload resolution; the exact path depends on the overloads you hit, but the value category story is the same: temporaries, moves, in-place construction.
std::unique_ptr
std::unique_ptr is the clean teaching example because copy is gone: only move construction and move assignment. The rvalue reference is not a micro-optimization here — it is the language feature that says “ownership transfers exactly once.” After the move, the old pointer is nullptr by contract, which is easier to reason about than “valid but unspecified” for some general-purpose string implementation.
auto p = std::make_unique<int>(42);
auto q = std::move(p); // p is null; q owns
Performance: copy vs move (benchmark sketch)
Microbenchmarks are implementation-dependent; this sketch shows how you might compare, not a universal truth.
#include <chrono>
#include <string>
#include <vector>
int main() {
using clock = std::chrono::high_resolution_clock;
const int n = 1'000'000;
long long t_copy = 0, t_move = 0;
for (int i = 0; i < n; ++i) {
std::string s(1000, 'x');
auto t0 = clock::now();
std::string a = s; // copy
t_copy += (clock::now() - t0).count();
t0 = clock::now();
std::string b = std::move(s);
t_move += (clock::now() - t0).count();
(void)a; (void)b;
}
// log t_copy vs t_move with your harness
return 0;
}
What to expect
- Large
std::string/std::vector: move is O(1) (few pointer assignments); copy is O(n). - SSO strings: copy and move can be similar (no heap).
- Always profile in your deployment configuration (
-O2/-O3, LTO, allocator).
Common confusions I’ve seen
The table version of this article would list “prefer Rule of Zero” and call it a day. In practice, the same misunderstandings walk into every review. People default move operations and then wonder why an exception-unsafe type still copies from vector — the answer is often noexcept, not a missing #include. People sprinkle std::forward in one place and std::move in another in the same forwarding layer, and then they are surprised when generic code copies. return std::move(local) is still common; it feels faster, but it is often how you talk the compiler out of elision. const values do not secretly become moves. After std::move, reading the old object without resetting is not “undefined” for all types, but it is a great way to ship “works on my machine” bugs. If you are optimizing hot loops, profile; with small strings, SSO can make moves and copies look identical in microbenchmarks.
If you want a single habit: when you write a forwarding function, write one pattern — deduced T&& plus std::forward<T> — and stop there until you can name the caller you are trying to optimize.
Questions I get asked
“Why am I getting a copy in my template?” Usually std::forward got dropped, or T deduced differently than you thought. Print the type in a static_assert once; it saves an afternoon.
“Why won’t T& bind to my temporary?” Because non-const lvalue references are not a storage lifetime strategy; add const T&, an rvalue reference overload, or take by value.
“Why did vector reallocate get slow after I added move?” If the move throws, the library may copy to keep the strong exception guarantee. Mark the move noexcept when that is honest.
“Why didn’t NRVO kick in?” Common culprits are return std::move(x) on a local, multiple return paths with different named objects, or returns that do not match the simpler elision patterns your compiler targets.
“Is this reference dangling?” If you returned const T& to a temporary, or stored a reference past a full expression, assume yes until you have walked the lifetime rules for that exact case.
“Why does T&& seem to accept everything in my template?” Check whether you really have a forwarding reference (deduced T&&) and whether T collapsed to an lvalue reference; if the intent was a normal rvalue parameter, spell it without the deduction surprise.
“I moved from this pointer class and now the process is corrupt.” Almost always a move that did not leave the source in a destroyable state — often missing nulls, or copying pointers in the move ctor by mistake.
Quick recap: the mental model
- Lvalue: usually has a name; you can’t implicitly treat it as a disposable resource unless you cast to xvalue.
- Prvalue: “pure” computation or literal; no identity until materialized.
- Xvalue: “expiring” glvalue;
std::moveand certain casts. - Rvalue ref
T&&(non-deduced): binds to rvalues; enables moves. - Forwarding ref
T&&(deduced):Tis reference for lvalue arguments; usestd::forward. - Move operations are selected by rvalue arguments;
std::moveonly enables selection.
Overload resolution: how value categories pick f(T&), f(const T&), and f(T&&)
When multiple overloads exist, the compiler will first build a viable set, then bicker about better conversions. I see the same three patterns in production code, over and over. A non-const lvalue will usually run toward T& if it can, then T const& if it must, and it will not bind cleanly to a plain T&& in the “I am disposable” way without a failed conversion story. A rvalue of type T wants the move path: T&& (move) beats T const& for many overload sets when both exist, because a move is the honest match to “this is expiring value stuff.” A pass-by-value g(T t) is its own world: a caller’s rvalue may move-construct t, a caller’s lvalue may copy (or the type is so cheap it does not matter).
The footgun I still see weekly: a named rvalue reference is an lvalue. The variable rr in T&& rr = /*...*/; is an lvalue, full stop. If you need to pass that along as an rvalue, you reach for std::move(rr) or the right std::forward — otherwise you are trying to hand someone a “temporary” that is not temporary at all, just awkwardly named.
void h(int&& v);
void demo() {
int&& rr = 5; // OK: 5 is prvalue, bound to rref
h(5); // OK: prvalue
// h(rr); // error: rr is lvalue
h(std::move(rr)); // OK: xvalue
}
std::swap and move
Standard std::swap is typically implemented in terms of move for move-assignable, move-constructible types: move one into a temp, move the other to the first, move temp to the second. If move is not noexcept but the type might be used in exception-sensitive algorithms, the standard library still carefully constrains which operations are noexcept — for your own swap ADL hooks, follow the same pattern and mark noexcept when the underlying members allow it.
= default and = delete for special members
Explicit move operations communicate intent and can fix implicitly deleted members when, for example, a user-declared destructor exists.
struct A {
~A() = default;
A(A&&) noexcept = default;
A& operator=(A&&) noexcept = default;
A(const A&) = default;
A& operator=(const A&) = default;
};
If you = delete the copy constructor to enforce unique ownership, the move may still be present (std::unique_ptr-style) or the type may be move-only.
Sinks and factory functions
Sink arguments (take ownership) often use by-value + move from the caller, or rvalue ref:
void take_string(std::string s);
void take_string_rref(std::string&& s);
void caller() {
std::string x = "data";
take_string(std::move(x));
take_string("literal"); // prvalue constructs s directly in some cases
}
Factory functions usually return a prvalue (return Widget(...);) to maximize elision; returning std::move(Widget) from a local is usually wrong; returning std::move from a member or from std::optional value() is a separate design discussion.
Concepts (C++20) and perfect forwarding
When generic code should only accept certain types, constrain T with concepts after you understand reference deduction:
#include <concepts>
#include <utility>
template<std::move_constructible T>
void emit(T&& x) {
T sink = std::forward<T>(x);
(void)sink;
}
std::decay / std::remove_reference often appear in metaprogram when storing T in a std::tuple or in type lists.
Memory order (brief note)
Value categories and rvalue references are unrelated to std::memory_order and atomics. Do not conflate move in the concurrency sense (atomics) with C++ move semantics; only the name overlaps in English.
Aggregates, designated initializers, and rvalues (C++20)
Aggregate initialization can produce prvalues of class type. When you pass that prvalue to const T& or T&&, materialization and overload resolution follow the same rules as for a factory return expression. The important lesson for value categories is unchanged: the expression (before binding) has a category; the parameter in the function body is an lvalue when it is a named parameter.
Coroutines (C++20) and move
Coroutine frame storage and the movement of parameters into the coroutine state are driven by a generated signature and promise machinery; moves and copies can occur where the “naive” mental model of a single stack frame does not hold. If you work with co_await and types with non-trivial move, read your compiler’s documentation on parameter passing into coroutine frames — the value category of call-site arguments still drives which constructor runs before suspension.
std::optional, std::variant, and value() / get()
*optoropt.value()on an lvalueoptional→ generally lvalue access to the contained object.std::move(opt).value()(or the careful patterns from library docs) is used to obtain an rvalue to the contained value for a move, subject to the exact Standard wording and library implementation — a frequent source of “why didn’t it move” questions, so read cppreference for your language version.
When copies still happen (even with C++11 moves)
- Lvalue passed where only copy exists or move is not chosen.
constobject in contexts that require moving non-constresources.- Throwing move constructor: container reallocation may copy to provide strong exception guarantee.
- Subobjects and incomplete type edges in older code.
- Implicit conversions to
Tbefore move (extra temporaries) — can reduce to copies if the conversion path only has copy.
decltype vs std::decay in templates
decltype preserves top-level reference and const on id-expressions. When you need a “value type” for a container, you often use:
template<typename T>
using decay_t = std::remove_cv_t<std::remove_reference_t<T>>;
std::decay also converts arrays to pointers and function to pointer, which remove_reference alone does not.
More of what shows up in review (not a checklist)
For POD-ish aggregates with only trivial members, letting the compiler generate special members is usually the win. The moment you touch one of destructor / copy / move, step back and look at all five — half-implemented resource types are where the weird bugs live; crank warnings (-Wextra, -Weffc++ on GCC, /W4 on MSVC) so the compiler helps. In public APIs, returning by value and using move-only types for exclusive ownership tends to age well. In forwarding templates, I still see people thread move through every layer; one T&& plus std::forward<T> per hop is enough, and anything else needs a comment with a name attached. In inheritance, slicing and move are different problems; if a base owns resources, think virtual destructor and the full special-member story, not just a clever T&& overload.
Sanity script for a template (still the best trick I know): instantiate wrapper<int>, wrapper<int&>, and wrapper<const int&> in a test translation unit, then static_assert what you think T is. If that sounds boring, compare it to the cost of debugging a silent copy in a header-only library.
When someone says “my overloads are ambiguous,” I look for redundant T&/T&& sets or qualification weirdness first; a small static_cast to the reference you meant often ends the fight, or you delete an overload you did not need. If = delete on copy painted you into a corner but you still need copies, that is a design conversation, not a three-line fix. If forward looks wrong, nine times out of ten the template parameter in forward<T> is not the same T as in the function signature — copy-paste the name, do not retype it. If a function takes T&& and callers are surprised it can modify a temporary, that is not a compiler bug; immutability wants a different signature. If performance got worse after “adding moves,” look at allocators, SSO, and cache before you blame value categories — the hardware does not read cppreference.
Glossary (one line each)
- Materialization: turning a prvalue into a temporary object with storage.
- Temporary object: unnamed object usually destroyed at the end of the full-expression.
- Forwarding reference: deduced
T&&(orauto&&with deduced type) per the template reference rules. - Reference collapsing: combining
&and&&in typedef and template substitution. - Strong exception guarantee: operation either completes or has no effect (relevant for vector reallocation strategy).
Further reading: Keep cppreference value categories and your compiler’s C++ standard draft links bookmarked; corner cases (subobject lifetime, typeid, and unevaluated contexts) are specified there in full detail.
Original compact examples (reference)
int x = 10; // x is an lvalue; 10 is an rvalue
int& lref = x; // OK: lvalue reference
// int& lref2 = 10; // Error: cannot bind rvalue to lvalue reference
int&& rref = 10; // OK: rvalue reference
// int&& rref2 = x; // Error: cannot bind lvalue to rvalue reference
// Named and has an address
int v = 10;
int* ptr = &v; // OK
int arr[10];
std::string s;
int& ref = v;
*ptr;
// Rvalue / temporary; address of the literal is ill-formed
// int* p = &10; // Error
10;
// x + y; // (with x, y in scope) rvalue for arithmetic
// func(); // return by value: prvalue
std::move(v); // xvalue
void func(int& x) {
std::cout << "lvalue ref" << std::endl;
}
void func(int&& x) {
std::cout << "rvalue ref" << std::endl;
}
int main() {
int a = 10;
func(a);
func(10);
func(std::move(a));
}
class Buffer {
int* data;
size_t size;
public:
Buffer(size_t s) : size(s) { data = new int[size]; }
~Buffer() { delete[] data; }
Buffer(const Buffer& other) : size(other.size) {
data = new int[size];
std::copy(other.data, other.data + size, data);
std::cout << "copy" << std::endl;
}
Buffer(Buffer&& other) noexcept
: data(other.data), size(other.size) {
other.data = nullptr;
other.size = 0;
std::cout << "move" << std::endl;
}
};
int main() {
Buffer b1(100);
Buffer b2 = b1; // copy
Buffer b3 = std::move(b1); // move
}
#include <vector>
#include <string>
int main() {
std::vector<std::string> vec1;
vec1.push_back("Hello");
std::vector<std::string> vec2 = vec1;
std::vector<std::string> vec3 = std::move(vec1);
}
std::string getName() {
return "Alice";
}
int main() {
std::string name = getName();
const std::string& ref = getName();
}
// lvalue, prvalue, xvalue
int a = 0, b = 0;
a; // lvalue
10; // prvalue
a + b; // prvalue
std::move(a); // xvalue
static_cast<int&&>(a);
// glvalue = lvalue + xvalue; rvalue = prvalue + xvalue
std::vector<int> vec1 = {1, 2, 3};
std::vector<int> vec2 = std::move(vec1);
vec1 = {5, 6, 7};
const std::string s = "Hello";
// std::string s2 = std::move(s); // copies
std::string func() {
std::string s = "Hello";
return s;
// return std::move(s); // can break NRVO
}
template<typename T>
void f(T&& x) {
(void)x;
}
int y = 10;
f(y);
f(10);
template<typename T>
void wrapper(T&& arg) {
process(std::forward<T>(arg));
}
FAQ
Q1: Lvalue vs rvalue (colloquial)?
A: Lvalues usually have stable identity (a place in memory you can name). Rvalues in the colloquial sense are the things on the right of assignments or temporaries — the standard splits them into prvalues and xvalues.
Q2: What does std::move do?
A: Casts the argument to an rvalue ( xvalue ). No bytes move by itself.
Q3: State after a move?
A: Valid, unspecified — safe to assign or destroy; not always safe to read from without resetting.
Q4: const and move?
A: Moving from const T is not a logical move; typically copy.
Q5: Performance benefit?
A: Large owned buffers: huge. Trivial or SSO types: may be noise.
Q6: Learning resources?
A: Effective Modern C++ (Meyers), C++ Move Semantics (Klaus Iglberger / various authors in WG21 papers), cppreference.com value categories.
Related reading (internal links)
- C++ value categories
- C++ move semantics
- C++ copy/move constructors (Rule of Five)
- [C++ perfect forwarding](/en/blog/cpp-perfect-forwarding/
- C++ return statement
- C++ CTAD
Practical tips
- When debugging odd overload picks, log or inspect deduced
Tin astatic_assertor compiler diagnostic. - Reproduce in a minimal
.cppwith one moving part. - Profile before and after introducing moves in hot paths.
Keywords (search)
C++, rvalue, lvalue, xvalue, prvalue, glvalue, value category, move semantics, std::move, std::forward, Rule of Five, RVO — these queries should lead readers to the topics above.
Related posts
- C++ value categories
- C++ move semantics
- C++ references guide
- C++ move semantics series
- C++ algorithm copy