C++ Value Categories
Motivation
Consider the following code:
1 | struct Data { |
Explanationauto d4 = std::move(getData(42));
- Firstly,
Data(size_t s)
constructor is called bygetData()
function. Then the copy constructor is called becausegetData
function returns aconst Data
object, so the copy constructor creates a newData
object, andstd::move
moves new object ford4
.
Value Categories
Value Categories were inherited from C, with the porting of “lvalue expression”, which is originally referred to the location of expression with regards to assignment.
1 | auto a = int(42); |
- lvalue (left-value) was on the left of the assignment.
- rvalue (right-value) was on the right of the assignement.
Value Category of an entity defines:
- lifetime
- Can it be moved from
- Is it a temporary
- Is it observable after change, etc
- Identity
- Object has identity if its address can be taken and used safely
Value Categories affect two very important aspects:
Performance
Overload resolution
Value Category is a quality of an expression.
1 | struct Data { |
ExplanationData &&a
is rvalue reference
, but this is confusing because a
have a name we can take and change it, which means this is an lvalue
.
- a’s Type: rvalue reference to Data
- a’s Value Category: lvalue
The entity can have differentValue Category
in different contexts.
So let’s look at functionfoo
that takes data, during calling this function, we firstly create this unamed temporary data with 73,foo
function binds the temporary data object to this rvalue reference, now in this functionfoo
, this temporary entity has a name inside the scope of thefoo
function, so inside the scope of the foo function, this entity is an lvalue, because we can take its address we can assign to it. So the different scope gave different value category to the same entity.
Each expression has two properties:
- A TYPE (including CV qualifiers)
- A VALUE CATEGORY
Value Category is a quality of an expression.
Expression Category taxonomy
1 | expression |
- C++11: added rvalue references, move semantics:
Has Identity (glvalue) | Doesn’t have identity | |
---|---|---|
Can’t be moved from | lvalue | - |
Can’t be moved from (rvalue) | xvalue | prvalue |
- C++17: added guaranteed copy elision:
[P0135] Guaranteed copy elision through simplified value categories
Has Identity (glvalue) | Doesn’t have identity | |
---|---|---|
Can’t be moved from | lvalue | - |
Can’t be moved from (rvalue) | xvalue | prvalue materialization |
The result of a prvalue is the value that the expression stores into its context. So then we materialize this thing and we get the prvalue.
C++20:
[p0527]: Implicitly move from rvalue references in return statements
Moved Value Categories section from [basic] to [expt — expression]C++23:
[P0847]: Deducing this
TODO: Differences about value categories between C++11 and C++17 and C++20
TODO: Value Categories in C++17
Main Categories (classification only)
- glvalue: (have identity) expression whose evaluation determines the identity of an object or function.
- rvalue: (can be moved from) a prvalue or an xvalue
Subcategories
lvalue: glvalue that is not an xvalue
xvalue: glvalue that denotes an object whose resources can be reused (usually because it is near the end of its lifetime).
prvalue: expression whoes evaluation initializes an object, or computes the value of the operand of an operator, as specified by the context in which it appears, or an expression that has type cv void.
glvalue
1 | struct Data { int n; int* pn = &n; }; |
- prvalue
1 | struct Data { |
- xvalue
1 | struct Data { int n; int* pn = &n; }; |
The Details of Binding
- Expressions with different Value Categories “bind” to different types of References.
- The Reference type which binds the expression determines the permitted operations.
1 | int a = 42; |
- Binding rules are important as part of the following “events”:
- Initialization or assignment
- Function call (including non-static class member function called on an object)
- Return statement
So all of these three different events that happens in your code needs to take the value category and the binding rules under consideration.
Initialization or Assignment
1 | int a = 42; |
- Binding Rules
Binds lvalues? | Binds rvalues? | |
---|---|---|
lvalue reference | YES | NO |
const lvalue reference | YES | YES |
rvalue reference | NO | YES |
const rvalue reference | NO | YES |
rvalues can be bound to const lvalue reference and rvalue reference and to const rvalue reference. lvalues can be bound to lvalue reference and const lvalue reference. This is very important in the scope of talking about move constructor.
If a rvalue is bound to a const lvalue reference, it will extend its lifetime, rvalue reference can extend the lifetime of a temporary.
Limitations is the context of the function are according to the binding function:
Function can modify data?? | Caller can observe (old) data? | |
---|---|---|
lvalue reference | YES | YES |
const lvalue reference | NO | YES |
rvalue reference | YES | NO |
const rvalue reference | NO | NO |
Copy Elision Optimizations
Return Statement:
Starting from C++17, the behavior of VCs is affected by: “PO135 Guaranteed copy elision (…)”
There are two mandatory elisions of copy and move constructors:
Object Initialization
1
2
3Data d = Data{Data{42}};
// 1 CTOR (avoids: Copy CTOR)
// In C++17, copy constructor would be removed.Return Statement
An un-named Return Value Optimization (RVO):1
2
3Data getData(int x) { return Data{x}; }
Data d = getData(42);
// 1 CTOR (avoids Move CTOR)Return Statement: Materialization
Temporary materialization conversion conv.rval
A prvalue of type T can be converted to an xvalue of type T. This conversion initializes a temporary object of type T from the prvalue by evaluating the prvalue with the temporary object as its result object, and produces an xvalue denoting the temporary object.
In order to materialize, T shall be a complete type.
The Details of Binding
To summarize:
Binding rules apply in the following “events”:- initialization or assignment
- Function call (including non-static class member function called on an object)
- Return statements
Behavior of the entity(which :
The behavior of the entity is defined by the things that binds it.- Initialization: limits are according to the reference which binds it.
- Function call: limits inside the function are according to the overload which binds it.
- Return statement: limits as in initialization, with additional rules due to optimizations and const
Reference Collision
In case of concatenation of multiple ‘&’ symbols, such as in generic code, or in code using type aliases. Compiler performs Reference Collision.
1 | typedef int& lr; |
Forwarding Reference
Forwarding parameters inside a function template should consider Value Categories. The term for them was first suggested by Scott Myers, “universal reference”, and later, formalized as “forwarding reference“.
Due to TAD, “rvalue reference” has a special meaning in context of function template:
1 | // In the following code, T&& looks like rvalue reference, |
T&& keeps the value category of the type the instantiation is based on.
tools for Handling Value Categories
This tools helps you to manipulate value categories to understand better and to control them in your code.
1 | std::move |
std::move
Utility function, produces an xvalue expression T&&, this equivalent to static_cast
to a T
value reference type: static_cast<typename std::remove_reference<T>::type&&>(t)
Notice that std::move
may not always do what you hoped:
1 | void foo(int& x) { std::cout << "int&"; } |
std::forward
1 | std::forward<T>(expression); |
N1385 The forwarding problem: Arguments, presented two issues: forwarding params, and returning result. Suggested utility function, preserves value category of the object passed to the template.
It suggests a solution for the forwarding problem. In this paper, they’ve recognized that there is an issue and the value categories are something be that needs to be preserved, and suggested this utility.
std::forward
uses std::remove_reference<T>
to get the value type. std::forward
uses other utilities from the standard library in the implementation, and it’s commonly used combined with forwarding reference.
1 | // There are three overloads of the functions. |
std::decay
1 | std::decay<T>::type |
Type trait, result is accessible through _t
. Performs the following conversions:
- Array to pointer
- Function to function pointer
- lvalue to rvalue (removes cv qualifiers, references) (issue for move-only types)
it is the std::decay
is doing something very similar to what auto
is doing.
1 | template <typename T, typename U> |
This behavior should be familiar to you, as it resembles auto
s behavior (auto
performs auto-decay).
TODO
decltype specifier
1 | decltype( expression ); |
decltype
is a language thing, is a language utility, and it bascially gives you back the type of the object including value category, which is very important.
decltype
evaluates an expression, yields its type + value category (AKA the declared type).decltype
(unlike auto) preserves value category. For an expression of type T:
- If expression is xvalue, yields
T&&
. - If expression is lvalue, yields
T&
. - If expression is prvalue, yields
T
.
decltype
can be used instead of a type, as a placeholder which preserves value categories
1 | int&& foo(int& i) { return std::move(i); } |
- The T prvalue doesn’t materialize, so T can be an incomplete type (C++17).
- If evaluation fails (entity is not found or overload resolution fails), program is ill-formed.
((expression))
has a special meaning, and yields an lvalue expression.
1 | int&& a = 42; |
decltype
main use cases:
- When the type is unknown (syntax is available from C++14), we can use that to retrieve the type.
1
2
3
4
5
6
7
8template <typename T, typename U>
decltype(auto) Add(T t, U u) { return t + u; }
template <typename T>
decltype(auto) Wrapper(T&& t) {
// do something...
return std::forward<T>(t);
} - To preserve the value category of the expression.
1
2
3
4int && a = 32; // Type: rvalue ref to int | VC: lvalue
decltype(a) b = a; // Error! (binding rvalue ref to an lvalue ref a)
decltype(a) c = 73; // Type: rvalue ref to int | VC: lvalue
decltype((a)) d = a; // Type: lvalue ref to int | VC: lvalue
std::declval
1 | std::declval<T>( ) |
It basically takes an object and return the type.
Utility function produces:
xvalue expression
T&&
.If T is void, returns
T
.std::declval
can be used with expression to return the expression’s reference type.It can return a non-constructible or incomplete type.
1 | struct Type { |
So this is a way for you to communicate with compiler.
Combined with decltype
, we can get the type of a member (even when Type is non-constructible).
1 | decltype(std::declval<Type>().a) b = 73; |
std::declval
shouldn’t be used in an evaluated context (evaluating std::decltype
is an error).
std::declval
allows us to access T members, in a way preserves value categories.
1 | struct Type { |
std::decltype
and std::declval
are often used to transform between type and instance, for example:
Deducing This (C++23)
1 | template <typename T> |
P0847: Deducing this - voted into C++23this
allows specifying from within a member function the value category of the expression it’s invoked on
1 | struct Type { |
Combined with the forwarding reference, we can now write all these in a single template function.
1 | struct Type { |
This would help you to write libraries, or multiple overloads.
“Deducing this” feature introduced two new utilities: std::like_t
and std::forward_like<T>(u)
. std::like_t
applies CV and ref-qualifiers of T onto U. For example:
1 | std::like_t<double&, int>; // int& |
1 | std::forward_like<T>(U) -> std::forward<std::like_t<T, decltype(u)>>(u) |
References
C++ Value Categories