在 C++11 中,一共有 5 种 value:
它们的关系图:
lvalue 和 xvalue 合称为 glvalue。
prvalue 和 xvalue 合称为 rvalue。
The old saying: in C++ Every value is either an lvalue or an rvalue.
这句话今天仍然正确。不过现在更准确的说法是:
in C++ Every value is either an lvalue or an xvalue or a prvalue.
C++ 之父 Bjarne Stroustrup :
For all the values, there were only two independent properties:
There are four possible composition:
identity 可以理解为内存地址。因此 lvalue 和 xvalue 都是对应着实实在在的内存地址的,尽管对 xvalue 直接取地址 &xvalue
的语法是非法的。因此只能用间接的方式取得:
struct M { M* a = this;};cout << M().a << " " << M().a << endl;
以上的代码打印出了 xvalue M()
的内存地址。
等等!可能有一定基础的人会问:M()
应该是一个 prvalue,而不是 xvalue,作者是不是在胡扯?
看完后文就知答案。
举例:
int a = 1;int& b = a;int&& c = 1;int f() { return 1; }int& g() { return a; }int&& h() { return (int&&)a; }
int main() { a = 1; b = 1; c = 2; f(); g() = 2; h();}
int a = 1;
a
,并初始化为 value 1
。这里的 =
不是赋值语句,而是表示初始化。在 C++ 中赋值和初始化是两个不同的概念。1
叫做整型字面量(integer literal)。a.m
等)。int& b = a;
b
,并用 lvalue a
来初始化。这里的 =
不是赋值语句,而是表示初始化。表示让 b 成为 a 的一个 alias(别名),它们都对应同一个内存地址。b
是一个 named lvalue reference,它有名字,所以是 lvalueint&& c = 1;
c
,并用 rvalue 1
来初始化。这里的 =
不是赋值语句,而是表示初始化。表示让 c 成为 rvalue 1
的一个 alias(别名),它们都对应同一个内存地址。1
的内存地址是什么?如果 c 是在 stack 内定义的,则这个 rvalue 1
的地址就在 stack 中;如果 c 是在 global 内定义的,则这个 rvalue 1
的地址就在 static 中)c
是一个 named rvalue reference,它有名字,所以是 lvalueint f() { return 1; } 和 f();
f()
是一个 rvalue,它代表了一个没有名字的、临时的、 non-reference 变量。return 1(rvalue);
或者 return a(lvalue);
或者 return b(lvalue reference);
或者 return c(rvalue reference);
无关,仅仅与函数原型中的返回值类型 T 有关,即 T f() {...}
。f() = 1;
语法错误,因为 rvalue 不允许写在 built-in =
赋值语句的左边,这也是 rvalue 名字的由来。int& g() { return a; } 和 g() = 2;
g()
是一个 lvalue,它代表了一个临时的 unamed lvalue reference 变量。return a(lvalue);
或者 return b(lvalue reference);
或者 return c(rvalue reference);
无关,仅仅与函数原型中的返回值类型 T& 有关,即 T& f() {...}
。return 1;
,即只能 return lvalue,不能 return rvalue。这是因为,如果用 ?
来表示这个临时的 unamed lvalue reference,函数返回的过程可以理解为 int& ? = a;
,前面提到过,lvalue reference 只能用 lvalue 来初始化。f() = 1;
合法。lvalue 可以写在 built-in =
赋值语句的左边,这也是 lvalue 名字的由来。int&& h() { return (int&&)a; } 和 h();
h()
是一个 xvalue,它代表了一个临时的 unamed rvalue reference 变量。return 1;
或者 return f();
或者 return (int&&)a;
无关,仅仅与函数原型中的返回值类型 T&& 有关,即 T&& f() {...}
。return a 或 b 或 c;
,即只能 return rvalue,不能 return lvalue。这是因为,如果用 ?
来表示这个临时的 unamed rvalue reference,可以理解为 int&& ? = (int&&)a;
,前面提到过,rvalue reference 只能用 rvalue 来初始化。return 1
作为 h() 的返回值,因为 1
是存在于 h() 的 stack 的,h() 返回后,其 stack 就失效了,那 h() 这个临时变量的内存对应一个失效的 stack 地址。(int&&)a
等同于 static_cast<int&&>(a)
,表示把 lvalue a 强制转换成一个 rvalue,从而在语法上就可以把 (int&&)a 初始化给 int&& ?
了。在实质上 (int&&)a
和 a
还是对应同一个内存地址。std::move 就是这样实现的。h() = 1;
不合法。rvalue 不允许写在 built-in =
赋值语句的左边。left-hand-side value,左值,其特点是:
=
赋值语句的左边cout << &"www" << endl;
cout << &'w' << endl;
非法right-hand-side value,右值。xvalue 和 prvalue 合称 rvalue,他们的共同特点是:
=
赋值语句的右边,不能出现在左边=
赋值语句:string("hello")
是 rvalue,但可以出现在左边:string("hello") = string("world")
。注意,这个等号是 string class 定义的 operator=,不是 C++ built-ineXpiring value,如下四种情况会产生 xvalue:
(1) function calls that return rvalue references to objects
h()
,或者更常见的 std::move(t)
(2) casts to rvalue references to objects
(int&&)a
,或者更常见的 static_cast<T&&>(t)
(3) subscripting into a array xvalue。
int a[5]; std::move(a)[0];
(4) data member access into an xvalue of class type
std::move(x).m
是一个 xvaluestd::move(x).*mp
是一个 xvalue。mp 叫做 member object pointer。std::move(x).m
,仅限于 m
是普通 data member(non-static, non-reference),而不能是 member enumerator 或者 static member 或者 reference member 或者 static member function 或者 普通(non-static) member function。std::move(x).*mp
,仅限于 mp
是 pointer to data member,不能是 pointer to member function总结一下,(1)和(2)创建的都是 unamed rvalue references to objects (objects 不同于 functions)
(3)和(4)实际上都是取一个 xvalue 结构中的子数据块(data subobjects),得到的也是一个 xvalue。
在 rvalue 中,除了上面提到的 xvalue 的四种情况,其他都是 prvalue。例如:
true
),integer literal (例如 42
) 等等。const char *
)f()
function calla.f()
static or non-static member functionA()
包括各种构造函数str1 + str2
重载 operator 也相当于函数调用functor()
(对应 operator() 重载)[]{}()
lambda (对应 operator() 重载)a++
自增自减a+b
加减乘除a&b
位运算a&&b
逻辑运算a<b
关系运算&a
取地址a, b
comma expression (如果b 是 rvalue)(int)a
或 static_cast<int>(a)
强制类型转换a ? b : c
在某些情况下是 prvalueenum { yes, no };
中的 yes
, no
a.m
或 p->m
,其中 m
是 member enumeratora.m
或 p->m
或 a.*pm
或 p->*pm
,其中 m 和 mp 都对应普通成员函数(non-static)this
指针generalized lvalue, xvalue 和 lvalue 合称 glvalue,他们的共同特点是:
class unknown;unknown&& func(const unknown& u) { return (unknown&&)u;}
(unknown&&)u
就是一个 incomplete type回到前面是否在"胡扯"的问题,把 M().a 中的 M() 叫做 xvalue 合理吗?这个现象叫做 Temporary materialization,前方高能预警,可能刷新认知:
在某些情况下,一个 prvalue(必须是 complete type) 会被悄悄转换成 xvalue。也就是为一个本来不实际存在于内存中的对象(prvalue), 悄悄建立了一个匿名的、临时的、在内存中有地址的对象(xvalue)。这种行为是被 C++ 规范允许且明确定义了的,就叫做 Temporary Materialization。
Temporary Materialization 发生在下面几种情景中:
(1) when binding a reference to a prvalue
int&& c = 1;
const int& c = 1;
string&& k() { return string("hello"); }
这种用法仅限于举例说明,因为它返回了 callee stack 变量的引用,如果把该 unamed reference k()
理解成指针,则 k()
指向已经失效的 callee stack 中的地址。实际中极少用到,编译也会报警。注意不同于 RVO。(2) when performing a member access on a class prvalue
A().m;
pvalue A()
自动变成一个 xvlaueA().*mp;
pvalue A()
自动变成一个 xvlaue(3) when performing an (prvalue array)-to-pointer conversion or subscripting on an array prvalue
int (&& a)[2] = (int[]){1, 2};或者auto && a = (int[]){1, 2};
[&](int*){}((int[]){1,2});
((int[]){1, 2})[0];
下面的例子打印了 prvalue M() 的地址?
struct M { operator M*() { return this; }};cout << M() << endl;
这样 M()
的地址是可以打印出来的。不是说 M()
是 prvalue,没有地址的吗?
实际上,这里问题发生在 operator M*()
,它看似没有参数,实质上它的参数是 this,且概念上形式为 M().this
(1),符合 Temporary Materialization 的条件。
那如何理解 prvalue没有实际的内存地址 呢?我的理解是:当出现了一个 prvalue 时,编译器如果可以不为它分配内存地址就能实现,那就不为它分配内存地址;如果编译器必须为它分配一个内存地址才能实现,那就是一次 Temporary Materialization。
例如最简单的:
int a = 1;
其中 prvalue 1
就被实现成一个立即数,立即数显然是没有地址的。T a = T();
构造函数 T::T()
只被调用了一次,等号右边的 prvalue T()
根本没有存在过(或者理解成 T()
是直接在 a
的地址上构造出来的),没存在过显然也就没有地址。不过这种初始化的写法一般很少用,常见到的 T(xx)
形式的临时 object 一般都是 xvlaue,例如上例中的 M().a
和 M()
。以上 (1) 属个人猜想,请了解 compiler 的朋友指正。
下面几种表达式在不同情况下拥有的不同 value category:
函数
a.f
, p->f
, a.*pf
, p->*pf
, 其中 f
和 pf
都是普通成员函数(non-static),则它们都是 prvalue,这个我暂时还不理解为什么这样设计。struct M { void f() {} void (&r)() = f; };
语法错误struct M { void f() {} auto & r= f; };
语法错误void (M::*&mp)() = a.f;
语法错误auto & mp = a.f;
语法错误f();
就称为 function call):a.m
本身是 lvalue 还是 prvalue。a.m
a.m
都是 lvalue。例如:m 是 数据成员,或者 static成员函数。a.m
是 prvaluea.m
是 xvlauep->m
->
operatorp->m
都是 lvalue。例如:m 是 数据成员,或者 static成员函数。p->m
是 prvaluep->m
永远不会产生 xvalue,即使 p 是 xvaluea.*mp
a.*mp
是 lvaluea.*mp
是 xvaluea.*mp
是 prvaluep->*mp
->*
operatorp->*mp
是 lvaluep->*mp
是 prvaluep->*mp
永远不会产生 xvalue,即使 p 是 xvalue
思考题
std::forward<T>(t) 这个表达式是什么类型的 value?
References
本文介绍的比较简略,详细的介绍还是要看官方文档: