Primary value categories
lvalue
– Locator value
prvalue
– Pure rvalue
xvalue
– eXpiring value
C++ Day 2020 - online edition
Kris van Rens
(questions)
(questions)
struct Number {
int value_ = {};
};
class T {
public:
T(const Number &n) : n_{n} {}
T(const T &) { puts("Copy c'tor"); }
Number get() { return n_; }
private:
Number n_;
};
static T create(Number &&n) {
return T{std::move(n)};
}
int main() {
T x = T{create(Number{42})};
return x.get().value_;
}
What’s the output?
Value categories are not about objects or class types, they are about expressions!
An expression is a sequence of operators and their operands, that specifies a computation.
Expression evaluation may produce a result, and may generate a side-effect.
42 // Expression evaluating to value 42
17 + 42 // Expression evaluating to value 59
int a;
a = 23 // Expression evaluating to value 23
a + 17 // Expression evaluation to value 40
static_cast<float>(a) // Expression evaluating to floating-point value 23.0f
int a;
sizeof a // Expression evaluating to the byte size of 'a'
// Id-expression 'a' is unevaluated
[]{ return 3; } // Expression evaluating to a closure
printf("Hi!\n") // Expression evaluating to the number of characters written
// Result is often discarded, i.e. a 'discarded-value expression'
In C++, each expression is identified by two properties:
lvalue
– Locator value
prvalue
– Pure rvalue
xvalue
– eXpiring value
glvalue
– General lvalue
rvalue
– errrRrr..value
Value categories are organized based on expression properties:
int a;
a // Has identity
42 // Has no identity
nullptr // Has no identity
false // Has no identity
[]{ return 42; } // Has no identity
"Hi" // Has identity
std::cout // Has identity
a + 2 // Has no identity
a || true // Has no identity
a++ // Has no identity
++a // Has identity
static_cast<int>(a) // Has no identity
std::move(a) // Has identity
Expression result resources can be stolen if it evaluates to an anonymous temporary, or if the associated object is near the end of its lifetime.
This was the main motivation for move semantics
std::string func()
{
return "Steal me!";
}
std::vector<std::string> vec;
vec.push_back(func());
std::string x{"Steal me!"};
std::vector<std::string> vec;
vec.push_back(std::move(x));
42 // prvalue
nullptr // prvalue
"Hi there!" // lvalue
int x = 42;
++x // lvalue
x++ // prvalue
int x = 42;
x // lvalue
std::move(x) // xvalue
void func(int &&arg)
{
// 'arg' is an lvalue
// 'std::move(arg)' is an xvalue
other_func(std::move(arg));
}
func(42); // '42' is a prvalue
void func(int &arg); // #1
void func(int &&arg); // #2
int &&x = 42;
func(x); // Which overload is called?
Expression x
is an lvalue
; so overload #1 is called
lvalue
and rvalue
concepts,Please forget the right-/left-hand notion for today’s definition.
struct Number {
int value_ = {};
};
class T {
public:
T(const Number &n) : n_{n} {}
T(const T &) { puts("Copy c'tor"); }
Number get() { return n_; }
private:
Number n_;
};
static T create(Number &&n) {
return T{std::move(n)};
}
int main() {
T x = T{create(Number{42})};
return x.get().value_;
}
What’s the output?
A section in the C++ standard that describes the elision (i.e. omission) of copy/move operations, resulting in zero-copy pass-by-value semantics.
Restrictions apply
Permits elisions, it does not guarantee!
Actual results depend on compiler and compiler settings.
C++ code:
T func()
{
return T{}; // Create temporary
}
T x = func(); // Create temporary
Possible output (1):
T()
T(const &)
~T()
T(const &)
~T()
~T()
No copy elision.
C++ code:
T func()
{
return T{}; // Create temporary?
}
T x = func(); // Create temporary?
Possible output (2):
T()
T(const &)
~T()
~T()
Partial copy elision.
C++ code:
T func()
{
return T{};
}
T x = func();
Possible output (3):
T()
~T()
Full copy elision.
return
statement,throw
expression,catch
clause.Truth is; compilers have been doing it for years..
C++17 added mandates to the standard, informally known as:
A set of special rules for prvalue
expressions.
If, in an initialization of an object, when the initializer expression is a
prvalue
of the same class type as the variable type.
T x{T{}}; // Only one (default) construction of T allowed here
If, in a
return
statement the operand is aprvalue
of the same class type as the function return type.
T func()
{
return T{};
}
T x{func()}; // Only one (default) construction of T allowed here
Under the rules of C++17, a
prvalue
will be used only as an unmaterialized recipe of an object, until actual materialization is required.
A
prvalue
is an expression whose evaluation initializes/materializes an object.
This is called a temporary materialization conversion.
struct Person {
std::string name_;
unsigned int age_ = {};
};
Person createPerson() {
std::string name;
unsigned int age = 0;
// Get data from somewhere in runtime..
return Person{name, age}; // 1. Initial prvalue expression
}
int main() {
return createPerson().age_; // 2. Temporary materialization: xvalue
}
An implicit
prvalue
toxvalue
conversion.
prvalue
s are not moved from!
A variant of copy elision.
Two forms:
These terms live outside the standard.
Refers to the returning of temporary objects from a function.
Guaranteed by C++17 rules.
Refers to the returning of named objects from a function.
The most simple example
T func()
{
T result;
return result;
}
T x = func();
T()
~T()
Slightly more involved
T func()
{
T result;
if (something)
return result;
// ...
return result;
}
T x = func();
T()
~T()
It still works
Multiple outputs
T func()
{
T result;
if (something)
return {}; // prvalue
return result; // lvalue
}
T x = func();
Output stored elsewhere
static T result;
T func()
{
return result;
}
T x = func();
Slicing
struct U : T { /* Additional members */ };
T func()
{
U result;
return result;
}
T x = func();
Returning a function argument
T func(T arg)
{
return arg;
}
T x = func(T{});
When even NRVO is not possible..
T func(T arg)
{
return arg;
}
T x = func(T{});
T()
T(&&)
~T()
~T()
Implicit rvalue
conversion!
operator=
,struct Number {
int value_ = {};
};
class T {
public:
T(const Number &n) : n_{n} {}
T(const T &) { puts("Copy c'tor"); }
Number get() { return n_; }
private:
Number n_;
};
static T create(Number &&n) {
return T{std::move(n)};
}
int main() {
T x = T{create(Number{42})};
return x.get().value_;
}
What’s the output?
struct Number {
int value_ = {};
};
class T {
public:
T(const Number &n) : n_{n} {}
Number get() { return n_; }
private:
Number n_;
};
int main() {
T x = T{Number{42}};
return x.get().value_;
}
What’s the output?
prvalue
s are not moved from.Thank you