



The goal of this quick reference is to collect in one place and organize information about value categories in C++, assignment, parameter passing and returning from functions. I tried to make this quick reference convenient to quickly compare and select one of solutions possible, this is why I made several tables here.

For introduction to the topic, please use the following links:

C++ rvalue references and move semantics for beginners

Rvalues redefined

C++ moves for people who don’t know or care what rvalues are

Scott Meyers. Effective Modern C++. 2015

Understanding Move Semantics and Perfect Forwarding: Part 1

Understanding Move Semantics and Perfect Forwarding: Part 2

Understanding Move Semantics and Perfect Forwarding: Part 3

Do we need move and copy assignment

Value categories

Starting with С++ 11, any expression can belong only to one of three value categories: lvalue, xvalue or prvalue:

xvalue (expiring value) — expression, which identifies a non-temporary object, which can be moved

lvalue (left value) — expression, which identifies a non-temporary object of function, which cannot be moved

prvalue (pure rvalue) — expression, which initializes an object

These three categories can be grouped into three overlapping groups:

glvalue (generalized lvalue) groups lvalue and xvalue. glvalue has address in memory (has identity) and thus usually can be assigned a value (if it is not const)

rvalue groups prvalue and xvalue. rvalue can be either moved (xvalue) or does not belong to an existing object at all (prvalue). rvalue can be passed to move constructors, move assignment operators or move functions

So, xvalue has an address in memory and can be moved.

You can see from the table above, that named variables of lvalue, lvalue reference and rvalue reference categoties all have the same category of expression: lvalue (highlighted with red). For example, this means, that when rvalue reference is passed to a function, an lvalue reference overload will be chosen: T&& x=T(); f(x);

Links:

C++ lvalue rvalue xvalue glvalue prvalue

Value categories in C++ 17

Value categories

Abbreviations in this article

Abbreviations of constructors, operators and destructors:

Dc — Default constructor

Pc — Parameterized constructor

Cс — Copy constructor

Ca — Copy assignment (can reuse allocated storage, e.g. for std::string)

Mc — Move constructor

Ma — Move assignment

Va — Conversion assignment operator

D — Destructor

Abbreviations of value categories:

LV — Lvalue: T LV;

LR — Lvalue reference: T& LR = LV;

XV — Xvalue: move(LV)

PRV — Pure rvalue. Literal or result of function call, which return type is not a reference: T f() { return T(); }

FR — Forwarding reference: auto&& FR = LV;

CLV — const LV. CLR — const LR. CXV — const XV.

CPRV — const PRV. CPRV exists only for classes and arrays. Non-class non-array CPRV like cont int will be implicitly converted to PRV like int , e.g. in return value of function const int f() )

will be implicitly converted to PRV like , e.g. in return value of function ) RV is XV or PRV

CRV is CXV or CPRV

Other abbreviations:

PF — Perfect forwarding (using templated forwarding reference to pass parameter to function)

Assigning value categories

Assigning of value categories can be seen as the basis of all the following sections. When explicitly assigning one category to another without explicit category convertion, an implicit conversion takes place: FROM_TYPE x; TO_TYPE y; y = x; In the table below you can see, what happens when expression of type From is assigned to a variable of type To:

A minus sign means that such an assignment is not possible

A plus sign means that such an assignment is possible and it will not require calling of copy/move operators or constructors.

In other cases there is text, which means which copy/move operators and constructors will be called during such an assignment.

Conversions, which cannot occur implicitly from returned expression category to function return value type, are marked with red font — please see section "Returning from a function" below for details.

Lines with constant types are highlighted with blue.

Footnotes:

1 — Const auto&& references are not forwarding references, but const rvalue references, so they will act like const T&&

2 — a temporary created to hold a reference initializer persists until the end of its reference’s scope (see next section). Does not work for converting return value to function type.

3 — when passing literal, needs type specifier on the right for auto: auto&& = T{2};

Note:

Non-const lvalue and rvalue references cannot accept any const types. But non-const non-reference type can accept const types.

Rvalue reference (T&&) can accept xvalue or prvalue (XV or PRV).

Lvalue reference (T&) can accept lvalue or lvalue reference (LV or LR)

Non-reference types accept any types by converting, copying, moving or RVO

Const lvalue references (const T&, const auto&) and auto forwarding references (auto&&) accept any types without copy or move

Const and non-const rvalue references (T&&, const T&&) cannot accept const or non-const lvalues (LV or CLV).

LR cannot accept const or non-const rvalue references (XV or PRV or CXV or CPRV). CLR can accept them.

Types with const modifiers cannot be assigned to any types without const modifiers (except auto, which can become const if const is explicit on the right side).

Passing prvalue (passing return value, literal or constructor result) is the same as passing xvalue, but avoids calling move constructor when assigning to non-reference type

Passing const prvalue (passing return value of const type function) is the same as passing const xvalue, but avoids calling copy constructor when assigning to non-reference type

Auto references can become const, so they can accept both const and non-const variants. Also, they can become const references to make temporary persist until the end of reference's scope. But auto * references cannot become const if this is not explicitly specified on the right side. This is why you cannot assign rvalue to auto&

Const reference can receive a temporary and make it persist until the end of the reference scope (non-const reference cannot). This works only inside current scope (local function references and function arguments). If reference is a member of a class, temporary object will be destructed at the end of constructor or function and reference will continue pointing to a destructed object, which is undefined behaviour.

Auto x = CLV; will deduce auto to non-const type.

Examples and tests with printing of each called constructor and operator #include <iostream> #include <iomanip> #include <map> #include <vector> #include <string> using namespace std; template<class C, class T> auto contains(const C& v, const T& x) -> decltype(end(v), true) { return end(v) != std::find(begin(v), end(v), x); } template <class... Types> constexpr inline __attribute__((__always_inline__)) int UNUSED(Types&&...) { return 0; }; map<string, map<string, string>> res; vector<string> froms; vector<string> tos; string from; string to; void report(string st) { if (!from.empty() && !to.empty()) { res[from][to] += st; } cout << st << " "; } struct T { T() { report("Dc"); } T(int va) : a(va) { report("Pc"); } T(const T& other) : a(other.a) { report("Cc"); } T(T&& other) : a(std::exchange(other.a, 0)) { report("Mc"); } T& operator=(int va) { report("Va"); a = va; return *this; } T& operator=(const T& rhs) { report("Ca"); // check for self-assignment if(&rhs == this) return *this; a = rhs.a; return *this; } T& operator=(T&& rhs) { report("Ma"); // check for self-assignment if(&rhs == this) return *this; a = std::exchange(rhs.a, 0); return *this; } ~T() { report("D"); } int a = 1; }; T Fprv() { return T(); } const T Fcprv() { return T(); } void print_col(const string &st, int width) { cout << endl << left << setw(width) << st; } void test_assign(string lto, string lfrom) { from = lfrom; to = lto; res[from][to] = ""; if (!from.empty() && !to.empty()) { if (!contains(froms, from)) froms.push_back(from); if (!contains(tos, to)) tos.push_back(to); } print_col(lto + " = " + lfrom + ": ", 20); } #define TEST_ASSIGN(t, v) { \ test_assign(#t, #v); \ t s = v; \ cout << s.a; \ UNUSED(s); \ cout << "-"; \ } void test_conversion() { T l; const T cl; T& lr = l; const T& clr = l; T&& rr = T(); const T&& crr = T(); auto &&fr = T(); TEST_ASSIGN(T, 8); TEST_ASSIGN(T, T()); TEST_ASSIGN(T, l); TEST_ASSIGN(T, move(l)); TEST_ASSIGN(T, cl); TEST_ASSIGN(T, move(cl)); TEST_ASSIGN(T, lr); TEST_ASSIGN(T, move(lr)); TEST_ASSIGN(T, clr); TEST_ASSIGN(T, move(clr)); TEST_ASSIGN(T, rr); TEST_ASSIGN(T, move(rr)); TEST_ASSIGN(T, crr); TEST_ASSIGN(T, move(crr)); TEST_ASSIGN(T, Fcprv()); TEST_ASSIGN(T, Fprv()); TEST_ASSIGN(T, fr); TEST_ASSIGN(T, move(fr)); TEST_ASSIGN(const T, 8); TEST_ASSIGN(const T, T()); TEST_ASSIGN(const T, l); TEST_ASSIGN(const T, move(l)); TEST_ASSIGN(const T, cl); TEST_ASSIGN(const T, move(cl)); TEST_ASSIGN(const T, lr); TEST_ASSIGN(const T, move(lr)); TEST_ASSIGN(const T, clr); TEST_ASSIGN(const T, move(clr)); TEST_ASSIGN(const T, rr); TEST_ASSIGN(const T, move(rr)); TEST_ASSIGN(const T, crr); TEST_ASSIGN(const T, move(crr)); TEST_ASSIGN(const T, Fcprv()); TEST_ASSIGN(const T, Fprv()); TEST_ASSIGN(const T, fr); TEST_ASSIGN(const T, move(fr)); //TEST_ASSIGN(T&, 8); //TEST_ASSIGN(T&, T()); TEST_ASSIGN(T&, l); //TEST_ASSIGN(T&, move(l)); //TEST_ASSIGN(T&, cl); //TEST_ASSIGN(T&, move(cl)); TEST_ASSIGN(T&, lr); //TEST_ASSIGN(T&, move(lr)); //TEST_ASSIGN(T&, clr); //TEST_ASSIGN(T&, move(clr)); //TEST_ASSIGN(T&, rr); //TEST_ASSIGN(T&, move(rr)); //TEST_ASSIGN(T&, crr); //TEST_ASSIGN(T&, move(crr)); //TEST_ASSIGN(T&, Fcprv()); //TEST_ASSIGN(T&, Fprv()); TEST_ASSIGN(T&, fr); //TEST_ASSIGN(T&, move(fr)); TEST_ASSIGN(const T&, 8); TEST_ASSIGN(const T&, T()); TEST_ASSIGN(const T&, l); TEST_ASSIGN(const T&, move(l)); TEST_ASSIGN(const T&, cl); TEST_ASSIGN(const T&, move(cl)); TEST_ASSIGN(const T&, lr); TEST_ASSIGN(const T&, move(lr)); TEST_ASSIGN(const T&, clr); TEST_ASSIGN(const T&, move(clr)); TEST_ASSIGN(const T&, rr); TEST_ASSIGN(const T&, move(rr)); TEST_ASSIGN(const T&, crr); TEST_ASSIGN(const T&, move(crr)); TEST_ASSIGN(const T&, Fcprv()); TEST_ASSIGN(const T&, Fprv()); TEST_ASSIGN(const T&, fr); TEST_ASSIGN(const T&, move(fr)); TEST_ASSIGN(T&&, 8); TEST_ASSIGN(T&&, T()); //TEST_ASSIGN(T&&, l); TEST_ASSIGN(T&&, move(l)); //TEST_ASSIGN(T&&, cl); //TEST_ASSIGN(T&&, move(cl)); //TEST_ASSIGN(T&&, lr); TEST_ASSIGN(T&&, move(lr)); //TEST_ASSIGN(T&&, clr); //TEST_ASSIGN(T&&, move(clr)); //TEST_ASSIGN(T&&, rr); TEST_ASSIGN(T&&, move(rr)); //TEST_ASSIGN(T&&, crr); //TEST_ASSIGN(T&&, move(crr)); //TEST_ASSIGN(T&&, Fcprv()); TEST_ASSIGN(T&&, Fprv()); //TEST_ASSIGN(T&&, fr); TEST_ASSIGN(T&&, move(fr)); TEST_ASSIGN(const T&&, 8); TEST_ASSIGN(const T&&, T()); //TEST_ASSIGN(const T&&, l); TEST_ASSIGN(const T&&, move(l)); //TEST_ASSIGN(const T&&, cl); TEST_ASSIGN(const T&&, move(cl)); //TEST_ASSIGN(const T&&, lr); TEST_ASSIGN(const T&&, move(lr)); //TEST_ASSIGN(const T&&, clr); TEST_ASSIGN(const T&&, move(clr)); //TEST_ASSIGN(const T&&, rr); TEST_ASSIGN(const T&&, move(rr)); //TEST_ASSIGN(const T&&, crr); TEST_ASSIGN(const T&&, move(crr)); TEST_ASSIGN(const T&&, Fcprv()); TEST_ASSIGN(const T&&, Fprv()); //TEST_ASSIGN(const T&&, fr); TEST_ASSIGN(const T&&, move(fr)); //TEST_ASSIGN(auto&, T{8}); //TEST_ASSIGN(auto&, T()); TEST_ASSIGN(auto&, l); //TEST_ASSIGN(auto&, move(l)); TEST_ASSIGN(auto&, cl); TEST_ASSIGN(auto&, move(cl)); TEST_ASSIGN(auto&, lr); //TEST_ASSIGN(auto&, move(lr)); TEST_ASSIGN(auto&, clr); TEST_ASSIGN(auto&, move(clr)); TEST_ASSIGN(auto&, rr); //TEST_ASSIGN(auto&, move(rr)); TEST_ASSIGN(auto&, crr); TEST_ASSIGN(auto&, move(crr)); TEST_ASSIGN(auto&, Fcprv()); //TEST_ASSIGN(auto&, Fprv()); TEST_ASSIGN(auto&, fr); //TEST_ASSIGN(auto&, move(fr)); TEST_ASSIGN(const auto&, T{8}); TEST_ASSIGN(const auto&, T()); TEST_ASSIGN(const auto&, l); TEST_ASSIGN(const auto&, move(l)); TEST_ASSIGN(const auto&, cl); TEST_ASSIGN(const auto&, move(cl)); TEST_ASSIGN(const auto&, lr); TEST_ASSIGN(const auto&, move(lr)); TEST_ASSIGN(const auto&, clr); TEST_ASSIGN(const auto&, move(clr)); TEST_ASSIGN(const auto&, rr); TEST_ASSIGN(const auto&, move(rr)); TEST_ASSIGN(const auto&, crr); TEST_ASSIGN(const auto&, move(crr)); TEST_ASSIGN(const auto&, Fcprv()); TEST_ASSIGN(const auto&, Fprv()); TEST_ASSIGN(const auto&, fr); TEST_ASSIGN(const auto&, move(fr)); TEST_ASSIGN(auto&&, T{8}); TEST_ASSIGN(auto&&, T()); TEST_ASSIGN(auto&&, l); TEST_ASSIGN(auto&&, move(l)); TEST_ASSIGN(auto&&, cl); TEST_ASSIGN(auto&&, move(cl)); TEST_ASSIGN(auto&&, lr); TEST_ASSIGN(auto&&, move(lr)); TEST_ASSIGN(auto&&, clr); TEST_ASSIGN(auto&&, move(clr)); TEST_ASSIGN(auto&&, rr); TEST_ASSIGN(auto&&, move(rr)); TEST_ASSIGN(auto&&, crr); TEST_ASSIGN(auto&&, move(crr)); TEST_ASSIGN(auto&&, Fcprv()); TEST_ASSIGN(auto&&, Fprv()); TEST_ASSIGN(auto&&, fr); TEST_ASSIGN(auto&&, move(fr)); TEST_ASSIGN(const auto&&, T{8}); TEST_ASSIGN(const auto&&, T()); //TEST_ASSIGN(const auto&&, l); TEST_ASSIGN(const auto&&, move(l)); //TEST_ASSIGN(const auto&&, cl); TEST_ASSIGN(const auto&&, move(cl)); //TEST_ASSIGN(const auto&&, lr); TEST_ASSIGN(const auto&&, move(lr)); //TEST_ASSIGN(const auto&&, clr); TEST_ASSIGN(const auto&&, move(clr)); //TEST_ASSIGN(const auto&&, rr); TEST_ASSIGN(const auto&&, move(rr)); //TEST_ASSIGN(const auto&&, crr); TEST_ASSIGN(const auto&&, move(crr)); TEST_ASSIGN(const auto&&, Fcprv()); TEST_ASSIGN(const auto&&, Fprv()); //TEST_ASSIGN(const auto&&, fr); TEST_ASSIGN(const auto&&, move(fr)); cout << endl; const int twidth = 9; cout << left << setw(twidth) << "From:"; for (const auto& lto : tos) { cout << left << setw(twidth) << lto; } cout << endl; for (const auto& lfrom : froms) { cout << left << setw(twidth) << lfrom; for (const auto& lto : tos) { if (!res.count(lfrom) || !res[lfrom].count(lto)) { cout << left << setw(twidth) << "-"; } else if (res[lfrom][lto].empty()) { cout << left << setw(twidth) << "+"; } else { cout << left << setw(twidth) << res[lfrom][lto]; } } cout << endl; } cout << endl; } int main() { test_conversion(); cout << endl; return 0; } /* Output: Dc Dc Dc Dc Dc T = 8: Pc 8-D T = T(): Dc 1-D T = l: Cc 1-D T = move(l): Mc 1-D T = cl: Cc 1-D T = move(cl): Cc 1-D T = lr: Cc 0-D T = move(lr): Mc 0-D T = clr: Cc 0-D T = move(clr): Cc 0-D T = rr: Cc 1-D T = move(rr): Mc 1-D T = crr: Cc 1-D T = move(crr): Cc 1-D T = Fcprv(): Dc 1-D T = Fprv(): Dc 1-D T = fr: Cc 1-D T = move(fr): Mc 1-D const T = 8: Pc 8-D const T = T(): Dc 1-D const T = l: Cc 0-D const T = move(l): Mc 0-D const T = cl: Cc 1-D const T = move(cl): Cc 1-D const T = lr: Cc 0-D const T = move(lr): Mc 0-D const T = clr: Cc 0-D const T = move(clr): Cc 0-D const T = rr: Cc 0-D const T = move(rr): Mc 0-D const T = crr: Cc 1-D const T = move(crr): Cc 1-D const T = Fcprv(): Dc 1-D const T = Fprv(): Dc 1-D const T = fr: Cc 0-D const T = move(fr): Mc 0-D T& = l: 0- T& = lr: 0- T& = fr: 0- const T& = 8: Pc 8-D const T& = T(): Dc 1-D const T& = l: 0- const T& = move(l): 0- const T& = cl: 1- const T& = move(cl): 1- const T& = lr: 0- const T& = move(lr): 0- const T& = clr: 0- const T& = move(clr): 0- const T& = rr: 0- const T& = move(rr): 0- const T& = crr: 1- const T& = move(crr): 1- const T& = Fcprv(): Dc 1-D const T& = Fprv(): Dc 1-D const T& = fr: 0- const T& = move(fr): 0- T&& = 8: Pc 8-D T&& = T(): Dc 1-D T&& = move(l): 0- T&& = move(lr): 0- T&& = move(rr): 0- T&& = Fprv(): Dc 1-D T&& = move(fr): 0- const T&& = 8: Pc 8-D const T&& = T(): Dc 1-D const T&& = move(l): 0- const T&& = move(cl): 1- const T&& = move(lr): 0- const T&& = move(clr): 0- const T&& = move(rr): 0- const T&& = move(crr): 1- const T&& = Fcprv(): Dc 1-D const T&& = Fprv(): Dc 1-D const T&& = move(fr): 0- auto& = l: 0- auto& = cl: 1- auto& = move(cl): 1- auto& = lr: 0- auto& = clr: 0- auto& = move(clr): 0- auto& = rr: 0- auto& = crr: 1- auto& = move(crr): 1- auto& = Fcprv(): Dc 1-D auto& = fr: 0- const auto& = T{8}: Pc 8-D const auto& = T(): Dc 1-D const auto& = l: 0- const auto& = move(l): 0- const auto& = cl: 1- const auto& = move(cl): 1- const auto& = lr: 0- const auto& = move(lr): 0- const auto& = clr: 0- const auto& = move(clr): 0- const auto& = rr: 0- const auto& = move(rr): 0- const auto& = crr: 1- const auto& = move(crr): 1- const auto& = Fcprv(): Dc 1-D const auto& = Fprv(): Dc 1-D const auto& = fr: 0- const auto& = move(fr): 0- auto&& = T{8}: Pc 8-D auto&& = T(): Dc 1-D auto&& = l: 0- auto&& = move(l): 0- auto&& = cl: 1- auto&& = move(cl): 1- auto&& = lr: 0- auto&& = move(lr): 0- auto&& = clr: 0- auto&& = move(clr): 0- auto&& = rr: 0- auto&& = move(rr): 0- auto&& = crr: 1- auto&& = move(crr): 1- auto&& = Fcprv(): Dc 1-D auto&& = Fprv(): Dc 1-D auto&& = fr: 0- auto&& = move(fr): 0- const auto&& = T{8}: Pc 8-D const auto&& = T(): Dc 1-D const auto&& = move(l): 0- const auto&& = move(cl): 1- const auto&& = move(lr): 0- const auto&& = move(clr): 0- const auto&& = move(rr): 0- const auto&& = move(crr): 1- const auto&& = Fcprv(): Dc 1-D const auto&& = Fprv(): Dc 1-D const auto&& = move(fr): 0- From: T const T T& const T& T&& const T&&auto& const auto&auto&& const auto&& 8 PcD PcD - PcD PcD PcD - - - - T() DcD DcD - DcD DcD DcD - DcD DcD DcD l CcD CcD + + - - + + + - move(l) McD McD - + + + - + + + cl CcD CcD - + - - + + + - move(cl) CcD CcD - + - + + + + + lr CcD CcD + + - - + + + - move(lr) McD McD - + + + - + + + clr CcD CcD - + - - + + + - move(clr)CcD CcD - + - + + + + + rr CcD CcD - + - - + + + - move(rr) McD McD - + + + - + + + crr CcD CcD - + - - + + + - move(crr)CcD CcD - + - + + + + + Fcprv() DcD DcD - DcD - DcD DcD DcD DcD DcD Fprv() DcD DcD - DcD DcD DcD - DcD DcD DcD fr CcD CcD + + - - + + + - move(fr) McD McD - + + + - + + + T{8} - - - - - - - PcD PcD PcD D D D D D */

Initializing constant references with temporary objects

C++ allows to initialize a constant reference with a temporary object. In this case lifetime of a temporary object will be extended. Example:

struct T { int i = 1; }; const T& t = T(); cout << t.i;

Yet, this lifetime extension works only up to the end of the block, where temporary object was created. For this reason, if a constant reference member of a class in initialized with a temporary object in a constructor, temporary object will be destructed at the end of constructor and reference will continue to point to a destructed object, which is undefined behavior:

class A { public: // Will not compile: value-initialization of reference type //A() : t() {} const T& t; }; class B { public: // Will compile in some compilers, but temporary object will be destructed at the end of constructor B() : t(T()) { cout << "In constructor: " << t.i << endl; } const T& t; }; class C { public: // Will compile, but temporary object will be destructed at the end of constructor // Address sanitizer will show the problem C() : t(std::move(T())) { cout << "In constructor: " << t.i << endl; } const T& t; }; C c; cout << "C: " << c.t.i << endl;

Without address sanitizer this program will output some garbage, and with address sanitizer an error will be shown. For this reason thi C++ feature should not be used or should be used with caution.

Links:

Reference initialization

Const References to Temporary Objects

About binding a const reference to a sub-object of a temporary

Go to Part 2