C++ Value Categories: lvalue, prvalue, xvalue, and Their Role in Function Returns and Move Semantics
The article explains how C++ value categories—lvalue, prvalue, and xvalue—govern function return handling, parameter passing, and object lifetimes, detailing hidden out‑parameters, copy‑elision, const‑reference lifetime extension, rvalue references, and the role of std::move in enabling move semantics.
This article explores the evolution of C++ value categories—lvalue, prvalue, and xvalue—by examining how the language and compiler handle function return values, parameter passing, and object lifetimes.
1. From C to C++: Stack Layout and Return Values
In C, function parameters and local variables are stored on the stack. The article shows a simple C function and its AMD64 assembly, illustrating that arguments and return values are passed via registers or stack slots.
void Demo() { int a = 0; long b = 1; short c = 2; }
Corresponding assembly (simplified):
push rbp mov rbp, rsp mov DWORD PTR [rbp-4], 0 mov QWORD PTR [rbp-16], 1 mov WORD PTR [rbp-18], 2 pop rbp ret
When a function returns a scalar, the value is placed in a register (e.g., eax ).
int Demo() { return 5; }
Assembly:
push rbp mov rbp, rsp mov eax, 5 pop rbp ret
2. Returning Complex Types
For structs, the compiler may allocate a hidden “out‑parameter” on the caller’s stack and pass its address to the callee. The callee constructs the object in that memory.
struct Test { long a, b; }; Test Demo() { Test t = {1, 2}; return t; }
Assembly (simplified):
push rbp mov rbp, rsp mov QWORD PTR [rbp-16], rdi ; store hidden out‑param address ... ; construct local t mov rax, QWORD PTR [rbp-16] ; return pointer pop rbp ret
If the caller receives the result in a variable, the compiler may copy from the temporary (xvalue) into the variable, then destroy the temporary.
Test t = Demo();
Resulting assembly shows an extra copy and a destructor call for the temporary.
3. Summary of Return‑Value Handling
When the result fits in a register, it is returned directly (prvalue).
When the result needs memory, the caller provides a hidden slot; the callee writes into it (treated as an lvalue).
If only a part of the result is used, the compiler creates an anonymous temporary (xvalue) that is destroyed after the expression.
4. References and Addressability
Const references can bind to any rvalue (including temporaries). The reference itself is an lvalue, but it extends the lifetime of the temporary it binds to.
const Test& ref = Demo(); // extends lifetime of the temporary
Non‑const lvalue references cannot bind to temporaries because temporaries have no addressable storage.
Test& r = Demo(); // illegal
Thus, a const reference to a return value behaves like a normal variable that owns its storage.
5. Rvalue References and Move Semantics
Rvalue references ( T&& ) were introduced to enable move semantics. When a function overload takes T&& , a direct function return value prefers that overload.
void f(const Test&); // for lvalues void f(Test&&); // for temporaries f(Demo()); // calls the rvalue‑reference overload
Inside a move constructor, the source object’s resources are transferred (shallow copy) and the source is left in a valid but unspecified state (often nullified).
Test(Test&& other) : buf_(other.buf_) { other.buf_ = nullptr; }
Using a variable to receive a return value now benefits from copy‑elision (C++17), so no extra temporary is created.
Test t = Demo(); // copy‑elision, no extra xvalue
6. std::move
std::move is a utility that casts an lvalue to an rvalue reference, allowing the programmer to explicitly request move semantics.
String s1; String s2 = std::move(s1); // forces move construction
Note that std::move does not change the object’s actual value category; it merely enables overload resolution to select the move constructor.
7. Practical Takeaways
Value categories exist to reconcile C‑style low‑level calling conventions with high‑level language semantics.
prvalue = pure rvalue (no storage), xvalue = “expiring” value with temporary storage, lvalue = named object with storage.
Const references extend the lifetime of temporaries; rvalue references enable move semantics.
Copy‑elision (C++17) removes unnecessary temporaries for return‑value initialization.
std::move is a cast, not a mover; the actual move occurs in the move constructor/assignment.
Tencent Cloud Developer
Official Tencent Cloud community account that brings together developers, shares practical tech insights, and fosters an influential tech exchange community.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.