Types and “value categories” are different things. Don’t mix them up!
l-value (locator value)"hello")
T a = 3; // a is l-value with type T
T& ra = a; // ra is l-value with type T&
T&& rr = 3; // rr is l-value with type T&&
std::string s = "Hello"; // "Hello" is l-value
Type&)
int& foo(int &i){
return i;
}
int arg = 0;
foo(arg) = 3;
For string literal, the compiler internally creates an array of type const char[N], where N is the length of the string plus 1 for the null terminator ('\0').
std::move():
void foo(int);
// r is an lvalue
void (&r)(int) = foo; // bind a reference to the function
r(); // calls foo
// rr is an lvalue
void (&&rr)(int) = foo; // bind an rvalue reference to the function
rr(); // calls foo
// ar is lvalue of type void(&)(int)
auto& ar = std::move(f);
* operator (i.e., what dereferencing a raw pointer yields)
int i = 1;
int* pi = &i;
*pi = 2;
pr-value (pure rvalue) int a = 3; // 3 is pr-value
class Foo {
...
};
void Bar(Foo);
Bar(Foo{}); // Foo {} is pr-value
int i = 1;
int* ri = &i;
&x gives you a temporary pointer value — the address of x.
x-value (expiring value)std::move():
std::string A = "Hello";
std::string&& B = std::move(A);
std::string A = "Hello";
std::string&& B = static_cast<std::string&&>(A);
Type&&)
std::string&& foo() {
std::string i = "aaapppppp";
return i;
}
std::move(obj).member // rvalue
#include <iostream>
class C {
public:
C() = default;
C(const C&) = delete;
C(C&&) = delete;
};
C createC() { return C{}; }
void takeC(C val) {}
int main(){
C c = createC(); // Ok since C++17 (error prior to C++17)
takeC(createC()); // Ok since C++17 (error prior to C++17)
}
However, all C++ versions can compile this code provided that copy and move constructors are enabled:
#include <iostream>
class C {
public:
C() { std::cout<< "DEFAULT CTOR\n"; };
C(const C&) { std::cout<< "COPY CTOR\n"; }
C(C&&) { std::cout<< "MOVE CTOR\n"; }
C& operator=(const C&) { std::cout << "COPY ASSIGN\n"; return *this; }
C& operator=(C&&) { std::cout << "MOVE ASSIGN\n"; return *this; }
};
C createC() {
return C{};
} // In C++17, no temporary object is created at this point
void takeC(C val) {}
int main(){
std::cout << "1st step:\n";
C c = createC();
std::cout << "\n2nd step:\n";
createC();
std::cout << "\n3rd step:\n";
takeC(createC());
}
g++ main.cc -std=c++11 -fno-elide-constructors && ./a.out gives:
1st step:
DEFAULT CTOR
MOVE CTOR
MOVE CTOR
2nd step:
DEFAULT CTOR
MOVE CTOR
3rd step:
DEFAULT CTOR
MOVE CTOR
MOVE CTOR
So conceptually, creatC() is effectively like the snippet below:
C creatC() {
C temp; // default ctor
C ret = std::move(temp); // move ctor
return ret;
}
g++ main.cc -std=c++11 && ./a.out and g++ main.cc -std=c++17 && ./a.out give:
1st step:
DEFAULT CTOR
2nd step:
DEFAULT CTOR
3rd step:
DEFAULT CTOR
createC():
C creatC() { return C{}; }
foo(const C& c);
C c = createC();
foo(createC());
creatC():
C{}) to the caller directly.C c = createC():
c) with the return object with move ctor.c) with prvalue.takeC(createC()):
const C& c) from the return object with move ctor.const C& c) with prvalue (materialize arises here).See C++ on Friday: A prvalue is not a temporary for more details.
void f(int) {}
class A {};
int main(){
void (*fptr)(int) = f;
void (&fr)(int) = f;
void (&&frr)(int) = f;
A a;
// cannot bind non-const lvalue reference of type ‘A&’ to an rvalue of type 'A'
auto& am = std::move(a);
// Okay
const auto& am = std::move(a);
// Okay
// -->because a function marked with std::move() is still an lvalue.
auto& ar = std::move(f);
}
std::move() because a function marked with std::move() is still an lvalue.| Member kind | Accessed from lvalue object | Accessed from rvalue object | | ———————————— | ————————— | ————————— | | non-static, non-reference member | lvalue | xvalue | | static or reference member | lvalue | lvalue |
std::move() for non-static, non-reference member:
std::vector<std::string> coll;
std::pair<std::string, std::string&&> sp;
coll.push_back(std::move(sp.first)); // move
coll.push_back(std::move(sp.second)); // move
coll.push_back(std::move(sp).first); // move
coll.push_back(std::move(sp).second); // move
std::move() for static and reference member
struct S {
static std::string statString;
std::string& refString;
}
S obj;
coll.push_back(std::move(obj.statString)); // move
coll.push_back(std::move(obj.refString)); // move
coll.push_back(std::move(obj).statString); // copy
coll.push_back(std::move(obj).refString); // copy
In generic code, you might not know whether members are static or references. Therefore, using the approach to mark the object with std::move() is less dangerous.

| lvalue reference | const lvalue reference | rvalue reference | const rvalue reference | |
|---|---|---|---|---|
| lvalue | 1 | 2 | n/a | n/a |
| const lvalue | n/a | 1 | n/a | n/a |
| prvalue | n/a | 3 | 1 | 2 |
| xvalue | n/a | 3 | 1 | 2 |
| const xvalue | n/a | 2 | n/a | 1 |
std::move()) to a function and there is no specific implementation for move semantics (declared by taking an rvalue reference), the usual copy semantics is used inside f(), taking the argument by const&.
We can only bind rvalue reference to rvalue. For example:
void rvFun(std::string&&);
std::string s{...};
rvFun(s); // Error
rvFun(std::move(s)); // Okay
However, note that sometimes, binding rvalue reference to lvalue seemingly works. For example:
rvFun("Hello"); // Okay. Why?
rvFun(std::string{"Hello"})
There is a hidden operation involved, because the type of the argument (array of five constant characters) does not match the type of the parameter. We have an implicit type conversion, performed by the string constructor, which creates a temporary object that does not have a name.
void rvFun(std::string&& str);
std::string s{...};
rvFun(s); // Error
rvFun(std::move(s)); // Okay
rvFun(std::string{"Hello"});
when we use the parameter str inside the function, we are dealing with an object that has a name. This means that we use str as an lvalue. We can do only what we are allowed to do with an lvalue:
void rvFun(std::string&& str) {
rvFun(str); // Error
}
We have to mark str with std::move() inside the function again:
void rvFun(std::string&& str) {
rvFun(std::move(str)); // Okay
}
decltypedecltype to Check the Type of Names (Entities)T entity, then decltype(entity) yields T.T& entity, then decltype(entity) yields T&.T&& entity, then decltype(entity) yields T&&.For example:
If we declare T entity; then
| | lvalue | prvalue | xvalue |
| ——– | —— | ——- | — |
| std::is_same<decltype(entity),T> | true | true | false |
| std::is_same<decltype(entity),T&> |false | false | false |
| std::is_same<decltype(entity),T&&> | false | false | true |
| lvalue | prvalue | xvalue | |
|---|---|---|---|
std::is_reference<decltype(entity)>::value |
false | false | true |
std::is_lvalue_reference<decltype(entity)>::value |
false | false | false |
std::is_rvalue_reference<decltype(entity)>::value |
false | false | true |
Note that decltype(entity) cannot be used to check expresion’s value category. For example:
std::string s = "Hi";
std::string& rs = s;
decltype(s) and decltype(rs) yield std::string and std::string&, respectively; however, both s and rs are lvalue. This is why we need decltype((expr)) to distinguish value category further.
decltype to Check the Value Category of an Expressiondecltype((expr)) yields T&;decltype((expr)) yields T.decltype((expr)) yields T&&;| lvalue | prvalue | xvalue | |
|---|---|---|---|
std::is_same<decltype((expr)),T> |
false | true | false |
std::is_same<decltype((expr)),T&> |
true | false | false |
std::is_same<decltype((expr)),T&&> |
false | false | true |
| lvalue | prvalue | xvalue | |
|---|---|---|---|
std::is_reference<decltype((expr))>::value |
true | false | true |
std::is_lvalue_reference<decltype((expr))>::value |
true | false | false |
std::is_rvalue_reference<decltype((expr))>::value |
false | false | true |
decltype((expr)) is not meant to describe the type of the value itself — it’s a mechanism for mapping an expression’s value category into a corresponding reference-qualified type.
The type it yields is useful only insofar as it encodes the value category.
| You want to know… | Use… | Result |
| ———————————————- | ——————————————- | —————————— |
| Entity’s declared type | decltype(name) (no parentheses) | e.g. int& |
| Expression’s mapped type (with value category) | decltype((expr)) | e.g. int&, int&&, or int |
| Expression type (drop references) | std::remove_reference_t<decltype((expr))> | e.g. int |
| Value category (lvalue/xvalue/prvalue) | See how decltype((expr)) behaves | maps to & / && / none |
int global = 100;
int& getRef() {
return global; // returns a reference → an lvalue
}
int main() {
getRef() = 5; // OK! Modifies 'global'
}

#include <iostream>
struct T {
T(int t) : _t(t) { std::cout << "T constructed\n"; }
T(const T& rhs) { _t = rhs._t; std::cout << "T copied\n"; }
~T() { std::cout << "T destroyed\n"; }
void show() { std::cout << "t = " << _t << "\n"; }
int _t;
};
struct M {
M(int m): _m(m) { std::cout << "M constructed\n"; }
M(const M& rhs) { _m = rhs._m; std::cout << "M copied\n"; }
M(M&&) { std::cout << "M moved\n"; }
~M() { std::cout << "M destroyed\n"; }
void show() { std::cout << "m = " << _m << "\n"; }
int _m;
};
template<class U>
U prvalue(int u) {
return U(u);
}
template<class U>
U& lvalue(int u) {
U* ptr = new U(u);
std::cout << "ptr = " << ptr << "\n";
return *ptr;
}
template<class U>
U&& xvalue(int u) {
U* ptr = new U(u);
std::cout << "ptr = " << ptr << "\n";
return std::move(*ptr);
}
int main() {
// Test movable object
std::cout << "################ returns prvalue ###############\n";
std::cout << "=== M ===\n";
{
M obj = prvalue<M>(1);
obj._m = 2;
obj.show();
std::cout << "Inside block\n";
}
std::cout << "\n=== const M& binding ===\n";
{
const M& obj = prvalue<M>(1);
std::cout << "Inside block\n";
}
std::cout << "\n=== M&& binding ===\n";
{
M&& obj = prvalue<M>(1);
std::cout << "Inside block\n";
}
std::cout << "\n=== const M&& binding ===\n";
{
const M&& obj = prvalue<M>(1);
std::cout << "Inside block\n";
}
std::cout << "\n################ returns lvalue ###############\n";
std::cout << "=== M ===\n";
{
M obj = lvalue<M>(1);
std::cout << "obj = " << &obj << "\n";
std::cout << "Inside block \n";
}
std::cout << "\n=== M& binding ===\n";
{
M& obj = lvalue<M>(1);
std::cout << "obj = " << &obj << "\n";
delete &obj;
std::cout << "Inside block\n";
}
std::cout << "\n=== const M& binding ===\n";
{
const M& obj = lvalue<M>(1);
std::cout << "obj = " << &obj << "\n";
delete &obj;
std::cout << "Inside block\n";
}
std::cout << "\n################ returns xvalue ###############\n";
std::cout << "=== M ===\n";
{
M obj = xvalue<M>(1);
std::cout << "obj = " << &obj << "\n";
std::cout << "Inside block\n";
}
std::cout << "\n=== const M& binding ===\n";
{
const M& obj = xvalue<M>(1);
std::cout << "obj = " << &obj << "\n";
delete &obj;
std::cout << "Inside block\n";
}
std::cout << "\n=== M&& binding ===\n";
{
M&& obj = xvalue<M>(1);
std::cout << "obj = " << &obj << "\n";
delete &obj;
std::cout << "Inside block\n";
}
std::cout << "\n=== const M&& binding ===\n";
{
const M&& obj = xvalue<M>(1);
std::cout << "obj = " << &obj << "\n";
delete &obj;
std::cout << "Inside block\n";
}
// Test non-movable object
std::cout << "################ returns prvalue ###############\n";
std::cout << "=== T ===\n";
{
T obj = prvalue<T>(1);
std::cout << "Inside block\n";
}
std::cout << "\n=== const T& binding ===\n";
{
const T& obj = prvalue<T>(1);
std::cout << "Inside block\n";
}
std::cout << "\n=== T&& binding ===\n";
{
T&& obj = prvalue<T>(1);
std::cout << "Inside block\n";
}
std::cout << "\n=== const T&& binding ===\n";
{
const T&& obj = prvalue<T>(1);
std::cout << "Inside block\n";
}
std::cout << "\n################ returns lvalue ###############\n";
std::cout << "=== T ===\n";
{
T obj = lvalue<T>(1);
std::cout << "obj = " << &obj << "\n";
std::cout << "Inside block \n";
}
std::cout << "\n=== T& binding ===\n";
{
T& obj = lvalue<T>(1);
std::cout << "obj = " << &obj << "\n";
delete &obj;
std::cout << "Inside block\n";
}
std::cout << "\n=== const T& binding ===\n";
{
const T& obj = lvalue<T>(1);
std::cout << "obj = " << &obj << "\n";
delete &obj;
std::cout << "Inside block\n";
}
std::cout << "\n################ returns xvalue ###############\n";
std::cout << "=== T ===\n";
{
T obj = xvalue<T>(1);
std::cout << "obj = " << &obj << "\n";
std::cout << "Inside block\n";
}
std::cout << "\n=== const T& binding ===\n";
{
const T& obj = xvalue<T>(1);
std::cout << "obj = " << &obj << "\n";
delete &obj;
std::cout << "Inside block\n";
}
std::cout << "\n=== T&& binding ===\n";
{
T&& obj = xvalue<T>(1);
std::cout << "obj = " << &obj << "\n";
delete &obj;
std::cout << "Inside block\n";
}
std::cout << "\n=== const T&& binding ===\n";
{
const T&& obj = xvalue<T>(1);
std::cout << "obj = " << &obj << "\n";
delete &obj;
std::cout << "Inside block\n";
}
return 0;
}