What you'll learn
Move-aware C++ depends on the category of an expression, not merely the type of an object. This lesson builds the precise model behind lvalues, prvalues, xvalues, reference collapsing, forwarding, copy elision, and lifetime-sensitive interfaces.
By the end, you'll be able to:
- Classify expressions as lvalues, xvalues, or prvalues
- Predict reference collapsing and value-category overload selection
- Use std::move and std::forward only at ownership boundaries
- Recognize copy elision, lifetime extension, and dangling reference traps
Expression value categories
An lvalue identifies an object whose identity remains available; an xvalue identifies an expiring object whose resources may be reused; a prvalue computes a value that can initialize an object. Glvalue means “has identity” and includes lvalues and xvalues. Rvalue includes xvalues and prvalues.
| Expression | Category | Reason |
|---|---|---|
name | lvalue | A named object has persistent identity |
42 | prvalue | Computes an integer value |
make_widget() | prvalue | Returned class value initializes a result |
std::move(name) | xvalue | Identifies name as eligible for reuse |
*pointer | lvalue | Identifies the pointed-to object |
Key idea
T&& is still an lvalue expression whenever you use its name. Names have identity; use std::move or std::forward to recover a different category deliberately.Reference binding and collapsing
An lvalue reference normally binds to lvalues. An rvalue reference binds to rvalues and can mark a source as transferable. When template substitution or a type alias forms a reference to a reference, collapsing rules produce one final reference type: any lvalue reference wins; only rvalue-reference plus rvalue-reference remains an rvalue reference.
| Combination | Collapses to |
|---|---|
T& & | T& |
T& && | T& |
T&& & | T& |
T&& && | T&& |
Category-sensitive overloads
Overloads can distinguish a read-only lvalue source from an expiring rvalue source. Avoid multiplying overloads without a real behavior difference; taking a small value by value is often simpler. For a sink that stores a string, a by-value parameter followed by one move gives callers a clear copy-or-move path.
#include <iostream>
#include <string>
#include <utility>
void inspect(const std::string&) { std::cout << "borrow lvalue\n"; }
void inspect(std::string&&) { std::cout << "consume rvalue\n"; }
int main() {
std::string name{"Ada"};
inspect(name);
inspect(std::string{"Bjarne"});
inspect(std::move(name));
}Watch out
inspect(std::move(name)), the callee is permitted to move from name. Do not add std::move merely to silence a copy; it announces that the old value is no longer needed.move versus forward
std::move unconditionally produces an xvalue. std::forward<T>preserves the original argument category when T&& is a forwarding reference— that is, an rvalue reference to a deduced, cv-unqualified template parameter.
#include <memory>
#include <utility>
template <typename T, typename... Args>
std::unique_ptr<T> create(Args&&... args) {
return std::make_unique<T>(std::forward<Args>(args)...);
}
struct Connection {
Connection(const char* host, int port);
};
auto connection = create<Connection>("localhost", 8080);Tip
Copy elision and return values
Since C++17, many prvalues initialize their destination directly with no temporary move or copy. Named return value optimization can also construct a named local result in the caller. Return local values normally; adding std::move to the return can inhibit elision.
#include <string>
#include <vector>
struct Report {
std::string title;
std::vector<int> values;
};
Report build_report() {
Report report{"Quarterly", {4, 7, 9}};
return report; // NRVO may construct directly in the caller
}
Report empty_report() {
return Report{"Empty", {}}; // guaranteed prvalue elision
}Lifetime extension and dangling
Binding a temporary directly to a local const reference can extend that temporary to the reference's scope. The rule does not generally pass through return statements, reference members initialized from constructor parameters, or non-owning views. A returned reference or string_view must point into storage that outlives the caller's use.
#include <string>
#include <string_view>
std::string make_name() { return "Modern C++"; }
int main() {
const std::string& safe{make_name()}; // temporary lifetime extends locally
std::string owner{"persistent"};
std::string_view view{owner}; // valid while owner remains alive and unchanged
// std::string_view dangling{make_name()}; // temporary string dies immediately
}Watch out
Recap & quick check
Key takeaways
- Value categories classify expressions: lvalues and xvalues have identity; prvalues compute values.
- Reference collapsing makes any lvalue-reference component produce an lvalue reference.
- std::move marks an expression reusable; std::forward preserves a deduced caller category.
- Return values normally and let copy elision avoid unnecessary transfers.
- Reference and view APIs must make owner lifetime relationships provable.
Quick check
1. What category is a named variable expression?
2. What does T& && collapse to?
3. What is std::forward for?
4. Why avoid return std::move(local)?
Next: Module 21 — Smart Pointers, Ownership Graphs & Allocators, where these transfer rules scale to complex resource structures.