Value Categories
C++ defines the following value categories:
- lvalues: expressions for locations of long-living objects or functions. These objects have identity, persist in memory and are addressable.
- prvalues: expressions for short-living values for initializations. prvalues themselves do not exist somewhere in memory, they do not denote objects. They are unmaterialized entities meant for initialization.
- xvalue: A special location, representing a (long-living) object, whose resources/values are no longer needed and can be reused. The guts of this object can be stolen.
The moment a prvalue (conceptual temporary) becomes an xvalue(temporary object), it is called materialization. The temporary materialization conversion is usually an implicit prvalue-to-xvalue conversion.
Anytime a prvalue is used where a lvalue or xvalue is expected, a temporary object is created and initialized with the prvalue.
Rules for binding references
Define:
struct X{};
X v;
const X c;
void f(X);
void f(X&);
void f(const X&);
void f(X&&);
void f(const X&&);A non-
constlvalue referenceX&takes only non-constlvalues.A rvalue reference
X&&takes only non-constrvalues.A
constlvalue referenceconst X&can take everything and serves as a fallback mechanism for move semantics.A
constrvalue reference takes both modifiable andconstrvalues (e.g.std::move(c)).A universal reference binds to all value categories and are usually used to forward arguments.
Overload resolution priority
| Call | f(X&) |
f(const X&) |
f(X&&) |
f(const X&&) |
f(T&&) |
|---|---|---|---|---|---|
f(v) |
1 | 3 | no | no | 2 |
f(c) |
no | 1 | no | no | 2 |
f(X{}) |
no | 4 | 1 | 3 | 2 |
f(std::move(v)) |
no | 4 | 1 | 3 | 2 |
f(std::move(c)) |
no | 3 | no | 1 | 2 |
Note that, universal reference is always the second best option.
Universal references and detail
Note that, when we declare arg as T&& (pronounced T-ref-ref) where T is function template type parameter, this is a universal(forwarding reference). It does not follow the rules of rvalue references.
Reference collapsing rules
For a non-const object v and a const object c the type T and the type of arg is deduced as follows:
T |
T&& |
arg |
|
|---|---|---|---|
foo(v) |
X& |
X& && |
X& |
foo(c) |
const X& |
const X& && |
const X& |
foo(X{}) |
X |
X && |
X&& |
foo(std::move(v)) |
X&& |
X&& && |
X&& |
foo(std::move(c)) |
const X&& |
const X&& && |
const X&& |
X& &becomesX&.X& &&becomesX&.X&& &becomesX&.X&& &&becomesX&&.
Copy elison
Copy elison omits copy and move constructors, resulting in zero-copy pass-by-value semantics.
Mechanics
The basic mechanics of Return Value Optimization(RVO) is as follows:
- The caller allocates space on the stack for the return value, passes the address to the callee.
- The callee constructs the result directly in that space.
In the below code snip,
#include <print>
#include <iostream>
struct X{
X(double val) : m_val{val} { std::cout << "\n" << "constructed at " << this; }
X(const X&){ std::println("X(const X&)"); }
X(X&&){ std::println("X(X&&)"); }
~X(){ std::cout << "\n" << "destructed at " << this; }
double m_val;
};
void f(X arg){
std::cout << "\n" << "&arg = " << &arg;
}
X g(){
X obj = X(10); // copy elison initializing obj
// from temporary
return obj; // NRVO
}
X h(){
return X(15); // URVO
}
int main(){
f(X(42));
X v1{g()}; // Copy elison initializing v1 from the result of g()
std::cout << "\n" << "&v1 = " << &v1;
X v2{h()}; // Copy elison initializing v2 from the result of h()
std::cout << "\n" << "&v2 = " << &v2;
return 0;
}It is important to note that:
- When a temporary(prvalue) is used to initialize an object, or function parameter, a copy is elided.
- Inside a function, when a prvalue is returned by value, URVO(unnamed return value optimization) is performed.
- Inside a function, when a lvalue is returned by value, NRVO(named return value optimization) is performed.
Challenge puzzle
Observe the below code snip. What is the output printed?
#include <iostream>
struct A {
A(int x) : x(x) {}
A(const A& a) { x = 1; }
A(A&& a) { x = 2; }
int x;
};
void foo(A t) {
std::cout << t.x;
}
A bar(){
A a(5);
return a;
}
A foobar(){
return A(6);
}
A&& baz(A&& arg){
return std::move(arg);
}
int main() {
A a(3);
foo(a);
foo(A(4));
std::cout << bar().x;
std::cout << foobar().x;
A result{ baz(A(7)) };
std::cout << result.x;
}For more such puzzles, visit getcracked.io