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
Starting with С++ 11, any expression can belong only to one of three value categories: lvalue, xvalue or prvalue:
These three categories can be grouped into three overlapping groups:
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 of constructors, operators and destructors:
Abbreviations of value categories:
T LV;
T& LR = LV;
move(LV)
T f() { return T(); }
auto&& FR = LV;
cont int
will be implicitly converted to PRV like int
, e.g. in return value of function const int f()
)Other abbreviations:
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:
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:
Auto x = CLV;
will deduce auto to non-const type.#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
*/
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
К сожалению, не доступен сервер mySQL