⚖️ Lvalues & Rvalues — concise tutorial
What are lvalues and rvalues?
Lvalues identify objects with identity (you can take their address). Rvalues are temporary values or expressions that don't have a persistent identity. Modern C++ refines these into:
- prvalue: pure rvalue (e.g.
42,std::string("hi")). - xvalue: expiring value (e.g.
std::move(x), function returningT&&). - glvalue: general lvalue (either lvalue or xvalue).
Value categories determine which references and overloads an expression can bind to and are the foundation for move semantics and perfect forwarding.
Why it matters
- Enables efficient resource transfer (move semantics) instead of expensive copies.
- Affects overload resolution and template behavior (forwarding references).
- Helps write correct, performant APIs: accept by value, const ref, or universal reference depending on intent.
Basic usage (short)
T&binds to lvalues only.T&&(rvalue reference) binds to rvalues/xvalues and is used for moves and perfect forwarding.std::move(x)castsxto an xvalue — it enables moving but does not move by itself.std::forward<T>(t)preserves the value category in templates (perfect forwarding).
Example quick snippets:
int a = 1; // 'a' is an lvalue
int& la = a; // OK: lvalue reference
int&& ra = 2; // OK: rvalue reference binds to prvalue
int&& ra2 = std::move(a); // xvalue
Minimal move example
#include <string>
#include <utility>
struct Buffer { std::string data; };
struct Holder {
std::string s;
Holder(std::string str) : s(std::move(str)) {}
Holder(Holder&& other) noexcept : s(std::move(other.s)) {}
Holder& operator=(Holder&& other) noexcept { s = std::move(other.s); return *this; }
};
Holder makeHolder(){ return Holder("hello"); }
// caller: Holder h = makeHolder(); // moved or elided
Notes: mark move operations noexcept when possible to allow containers (e.g., std::vector) to prefer moves during reallocation.
Perfect forwarding (short)
Use forwarding references in templates and std::forward to preserve lvalue/rvalue-ness:
#include <utility>
#include <string>
void consume(const std::string&); // lvalue overload
void consume(std::string&&); // rvalue overload
template<class T>
void wrapper(T&& arg){ consume(std::forward<T>(arg)); }
std::string s = "hi";
wrapper(s); // lvalue overload called
wrapper(std::string("x")); // rvalue overload called
Common pitfalls & tips
std::moveon an object you still need — the object enters a valid but unspecified state.- Missing
noexcepton move constructors can force containers to copy instead of move during growth. - Named rvalue references inside templates are lvalues; use
std::move/std::forwarddeliberately. - Don't return references to local (stack) variables — leads to dangling references.
- For small trivially-copyable types, moving offers no benefit; prefer simplicity.
Practical rule-of-thumb: design APIs with clear ownership semantics — accept by const T&, T, or T&& depending on whether you need a copy, a view, or to take ownership.
How to try / compile
Save examples into a file (one snippet per file) and compile with a modern compiler supporting C++17/20:
g++ -std=c++17 example.cpp -O2 -Wall -Wextra -o example
clang++ -std=c++17 example.cpp -O2 -Wall -Wextra -o example
If you experiment with coroutines or other newer features, use -std=c++20.
Exercises
- Implement a small RAII
Bufferthat manages a heap array; add copy and move constructors/assignments and push instances intostd::vector<Buffer>to observe moves vs copies. - Write a templated
emplace_and_consumethat constructs an object from forwarded args and consumes it; verify overloads for lvalues and rvalues. - Instrument move/copy constructors and observe how
std::vectorbehaves whennoexceptis omitted vs present.
References
- cppreference: https://en.cppreference.com/w/cpp/language/value_category
- Herb Sutter and C++ core guidelines on move semantics and forwarding
If you'd like, I can:
- add runnable examples under
docs/Cpp/examples/and a smallMakefile, or - expand the section on lifetime of temporaries with diagrams, or
- add tests demonstrating
std::vectorgrowth behavior with/withoutnoexcepton moves.
Tell me which you'd like next.
Value categories overview
- lvalue: an expression that identifies a persistent object (has identity). Example: a variable
int x— the expressionxis an lvalue. - prvalue (pure rvalue): a temporary value that initializes objects or computes values, e.g.
42,std::string("hi")(before C++17 prvalues materialize differently). - xvalue (expiring value): an object whose resources can be reused (e.g. result of
std::move(x)or a function returningT&&). It's an rvalue but represents an object with identity that is near end-of-life. - glvalue: general lvalue (either lvalue or xvalue). In the C++ taxonomy glvalue ∪ prvalue = rvalue?
Simple mapping:
- expression
x(wherexis a named variable) -> lvalue - literal
42,"abc"-> prvalue std::move(x)-> xvalue- result of
a+b-> prvalue
Why it matters: value categories determine which overloads and reference types an expression can bind to, and they are the foundation for move semantics and perfect forwarding.
Lvalue and rvalue references
- Lvalue reference:
T&— binds to lvalues only. - Rvalue reference:
T&&— can bind to rvalues and xvalues (usable for move semantics and overload resolution).
Examples:
int a = 1; // 'a' is an object (lvalue)
int& ra = a; // OK: lvalue reference
// int& rb = 2; // error: can't bind lvalue ref to prvalue
int&& rr = 2; // OK: rvalue reference binds to prvalue
int&& rr2 = std::move(a); // OK: std::move(a) is an xvalue
Reference collapsing rules (important for templates):
T& &->T&T& &&->T&T&& &->T&T&& &&->T&&
Move semantics and std::move
Move semantics let you transfer (steal) resources from temporaries or objects you no longer need, avoiding expensive deep copies.
- Provide a move constructor
T(T&&)and move assignmentT& operator=(T&&)when your type manages resources (heap memory, file handles, etc.). std::move(x)castsxto an rvalue (xvalue) to enable moving; it does not move by itself.
Example: a minimal movable class
#include <utility>
#include <string>
struct Buffer {
std::string data;
Buffer() = default;
Buffer(const Buffer&) = default; // copy
Buffer(Buffer&& other) noexcept : data(std::move(other.data)) {} // move
Buffer& operator=(Buffer&& other) noexcept { data = std::move(other.data); return *this; }
};
// Usage
Buffer make_buffer(){ Buffer b; b.data = "hello"; return b; }
Buffer b2 = make_buffer(); // move (or elide)
Buffer b3 = std::move(b2); // move from named object
Notes:
- Mark move operations
noexceptwhen possible to enable some containers (e.g.,std::vector) to use move instead of copy during reallocations. - After moving, the moved-from object must be left in a valid but unspecified state.
Perfect forwarding and std::forward
Perfect forwarding preserves the value category of arguments when you forward them through template wrappers.
- Use a forwarding reference (formerly called universal reference)
T&&in a template parameter:template<class T> void f(T&& t). - Use
std::forward<T>(t)to forwardtpreserving lvalue/rvalue-ness.
Example:
#include <utility>
#include <string>
void consume(const std::string& s) { /* lvalue overload */ }
void consume(std::string&& s) { /* rvalue overload */ }
template<class T>
void wrapper(T&& arg) {
consume(std::forward<T>(arg)); // forwards lvalue as lvalue, rvalue as rvalue
}
std::string s = "hi";
wrapper(s); // calls consume(const std::string&)
wrapper(std::string("x")); // calls consume(std::string&&)
Common pitfalls
- Using
std::moveon an object you still need — leaves it in a valid but unspecified state. - Binding rvalue references to temporaries in ways that extend lifetime incorrectly (be careful with returning references).
- Forgetting
noexceptfor move constructors/assignments can degrade performance (some containers fall back to copy during reallocation). - Overloading surprises: adding both
T&andT&&overloads may change overload resolution; named variables are lvalues even if their type isT&&inside templates. - Dangling references: never return references to local variables.
Guidelines and best practices
- Prefer value semantics and rely on move semantics for performance.
- Implement move operations only when your type manages resources; otherwise the compiler-generated ones are usually fine.
- Use
= defaultfor copy/move when appropriate. - Mark move constructors and move assignment
noexceptwhen they cannot throw. - Use
std::moveonly when you intend to transfer ownership; prefer not to overuse it for small trivially-copyable types. - Use perfect forwarding in generic wrapper functions to preserve value categories.
Examples
- Move constructor example (complete):
#include <string>
#include <iostream>
struct Holder {
std::string s;
Holder(std::string str) : s(std::move(str)) {}
Holder(const Holder&) = default;
Holder(Holder&& other) noexcept : s(std::move(other.s)) { std::cout << "moved\n"; }
};
int main(){
Holder a("hello");
Holder b = std::move(a); // prints "moved"
}
- Perfect forwarding example:
#include <utility>
#include <string>
#include <iostream>
void consume(const std::string& s){ std::cout << "lvalue: " << s << '\n'; }
void consume(std::string&& s){ std::cout << "rvalue: " << s << '\n'; }
template<class T>
void wrapper(T&& t) { consume(std::forward<T>(t)); }
int main(){
std::string s = "abc";
wrapper(s); // lvalue
wrapper(std::string("xyz")); // rvalue
}
Compile (recommended):
g++ -std=c++17 -O2 -Wall -Wextra examples.cpp -o examples
Exercises
- Implement a small
Bufferclass that manages a heap array, add copy and move constructors and assignments, and demonstrate moves in astd::vector<Buffer>. - Write a template wrapper
make_and_consumethat constructs an object from forwarded constructor args and passes it to a consumer; test with lvalues and rvalues. - Show how a missing
noexcepton move constructor affectsstd::vectorwhen growing (inspect whether copies or moves are used by instrumenting constructors).
References
- C++ standard sections on value categories and expressions
- cppreference: https://en.cppreference.com/w/cpp/language/value_category
- Herb Sutter: GotW / talks about rvalue references and move semantics