#include <utility> #include <type_traits> namespace cpp98 { struct A { }; A func() { return A(); } int main() { int i = 1; i = 2; // 3 = 4; const int j = 5; // j = 6; i = j; func() = A(); return 0; } } namespace cpp11 { #define is_lvalue(x) std::is_lvalue_reference<decltype((x))>::value #define is_prvalue(x) !std::is_reference<decltype((x))>::value #define is_xvalue(x) std::is_rvalue_reference<decltype((x))>::value #define is_glvalue(x) (is_lvalue(x) || is_xvalue(x)) #define is_rvalue(x) (is_xvalue(x) || is_prvalue(x)) void func(); int non_reference(); int&& rvalue_reference(); std::pair<int, int> make(); struct Test { int field; void member_function() { static_assert(is_lvalue(field), ""); static_assert(is_prvalue(this), ""); } enum Enum { ENUMERATOR, }; }; int main() { int i; int&& j = std::move(i); Test test; static_assert(is_lvalue(i), ""); static_assert(is_lvalue(j), ""); static_assert(std::is_rvalue_reference<decltype(j)>::value, ""); static_assert(is_lvalue(func), ""); static_assert(is_lvalue(test.field), ""); static_assert(is_lvalue("hello"), ""); static_assert(is_prvalue(2), ""); static_assert(is_prvalue(non_reference()), ""); static_assert(is_prvalue(Test{3}), ""); static_assert(is_prvalue(test.ENUMERATOR), ""); static_assert(is_xvalue(rvalue_reference()), ""); static_assert(is_xvalue(make().first), ""); return 0; } } namespace reference { int&& rvalue_reference() { int local = 1; return std::move(local); } const int& const_lvalue_reference(const int& arg) { return arg; } int main() { auto&& i = rvalue_reference(); // dangling reference auto&& j = const_lvalue_reference(2); // dangling reference int k = 3; auto&& l = const_lvalue_reference(k); return 0; } } namespace auto_decl { int non_reference() { return 1; } int& lvalue_reference() { static int i; return i; } const int& const_lvalue_reference() { return lvalue_reference(); } int&& rvalue_reference() { static int i; return std::move(i); } int main() { auto [s1, s2] = std::pair(2, 3); auto&& t1 = s1; static_assert(!std::is_reference<decltype(s1)>::value); static_assert(std::is_lvalue_reference<decltype(t1)>::value); int i1 = 4; auto i2 = i1; decltype(auto) i3 = i1; decltype(auto) i4{i1}; decltype(auto) i5 = (i1); static_assert(!std::is_reference<decltype(i2)>::value, ""); static_assert(!std::is_reference<decltype(i3)>::value, ""); static_assert(!std::is_reference<decltype(i4)>::value, ""); static_assert(std::is_lvalue_reference<decltype(i5)>::value, ""); auto n1 = non_reference(); decltype(auto) n2 = non_reference(); auto&& n3 = non_reference(); static_assert(!std::is_reference<decltype(n1)>::value, ""); static_assert(!std::is_reference<decltype(n2)>::value, ""); static_assert(std::is_rvalue_reference<decltype(n3)>::value, ""); auto l1 = lvalue_reference(); decltype(auto) l2 = lvalue_reference(); auto&& l3 = lvalue_reference(); static_assert(!std::is_reference<decltype(l1)>::value, ""); static_assert(std::is_lvalue_reference<decltype(l2)>::value, ""); static_assert(std::is_lvalue_reference<decltype(l3)>::value, ""); auto c1 = const_lvalue_reference(); decltype(auto) c2 = const_lvalue_reference(); auto&& c3 = const_lvalue_reference(); static_assert(!std::is_reference<decltype(c1)>::value, ""); static_assert(std::is_lvalue_reference<decltype(c2)>::value, ""); static_assert(std::is_lvalue_reference<decltype(c3)>::value, ""); auto r1 = rvalue_reference(); decltype(auto) r2 = rvalue_reference(); auto&& r3 = rvalue_reference(); static_assert(!std::is_reference<decltype(r1)>::value, ""); static_assert(std::is_rvalue_reference<decltype(r2)>::value, ""); static_assert(std::is_rvalue_reference<decltype(r3)>::value, ""); return 0; } } namespace cpp17 { class NonMoveable { public: int i = 1; NonMoveable(int i) : i(i) { } NonMoveable(NonMoveable&&) = delete; }; NonMoveable make(int i) { return NonMoveable{i}; } void take(NonMoveable nm) { return static_cast<void>(nm); } int main() { auto nm = make(2); auto nm2 = NonMoveable{make(3)}; // take(nm); take(make(4)); take(NonMoveable{make(5)}); return 0; } } int main() { cpp98::main(); cpp11::main(); reference::main(); auto_decl::main(); cpp17::main(); }
每一个C++表达式都有一个类型:42
的类型为int
,int i;
则(i)
的类型为int&
。这些类型落入若干类别中。在C++98/03中,每一个表达式都是左值或右值。html
左值(lvalue)是指向真实储存在内存或寄存器中的值的表达式。“l”指的是“left-hand side”,由于在C中只有lvalue才能写在赋值运算符的左边。相对地,右值(rvalue,“r”指的是“right-hand side”)只能出如今赋值运算符的右边。express
有一些例外,如const int i;
,i
虽然是左值但不能出如今赋值运算符的左边。到了C++,类类型的rvalue却能够出如今赋值运算符的左边,事实上这里的赋值是对赋值运算符函数的调用,与基本类型的赋值是不一样的。数组
lvalue能够理解为可取地址的值,变量、对指针解引用、对返回类型为引用类型的函数的调用等,都是lvalue。临时对象都是rvalue,包括字面量和返回类型为非引用类型的函数调用等。字符串字面量是个例外,它属于不可修改的左值。ide
赋值运算符左边须要一个lvalue,右边须要一个rvalue,若是给它一个lvalue,该lvalue会被隐式转换成rvalue。这个过程是理所固然的。函数
C++11引入了右值引用和移动语义。函数返回的右值引用,顾名思义,应该表现得和右值同样,可是这会破坏不少既有的规则:优化
rvalue是匿名的,不必定有存储空间,但右值引用指向内存中的具体对象,该对象还要被维护着;this
rvalue的类型是肯定的,必须是彻底类型,静态类型与动态类型相同,而右值引用能够是不彻底类型,也能够支持多态;spa
非类类型的rvalue没有cv修饰(const
和volatile
),但右值引用能够有,并且修饰符必须保留。指针
这给传统的lvalue/rvalue二分法带来了挑战,C++委员会面临选择:code
维持右值引用是rvalue,添加一些特殊规则;
把右值引用归为lvalue,添加一些特殊规则;
细化表达式类别。
上述问题只是冰山一角;历史选择了第三种方案。
C++11提出了表达式类别(value category)的概念。虽然名叫“value category”,但类别划分的是表达式而不是值,因此我从标题开始就把它译为“表达式类别”。C++标准定义表达式为:
An expression is a sequence of operators and operands that specifies a computation. An expression can result in a value and can cause side effects.
每一个表达式都是三种类别之一:左值(lvalue)、消亡值(xvalue)和纯右值(prvalue),称为主类别。还有两种混合类别:lvalue和xvalue统称范左值(glvalue),xvalue和prvalue统称右值(rvalue)。
#define is_glvalue(x) (is_lvalue(x) || is_xvalue(x)) #define is_rvalue(x) (is_xvalue(x) || is_prvalue(x))
C++11对这些类别的定义以下:
lvalue指定一个函数或一个对象;
xvalue(eXpiring vavlue)也指向对象,一般接近其生命周期的终点;一些涉及右值引用的表达式的结果是xvalue;
gvalue(generalized lvalue)是一个lvalue或xvalue;
rvalue是xvalue、临时对象或它们的子对象,或者没有关联对象的值;
prvalue(pure rvalue)是否是xvalue的rvalue。
变量、函数、数据成员的名字,包括右值引用类型的变量也是lvalue;
int i; int&& j = std::move(i); static_assert(is_lvalue(j), ""); static_assert(std::is_rvalue_reference<decltype(j)>::value, "");
函数调用或重载运算符表达式,其返回类型为左值引用类型,或函数的右值引用类型;
内置赋值、复合赋值、前置自增、前置自减运算符表达式;
内置数组下标表达式a[n]
和p[n]
(a
为数组类型,p
为指针类型),a
是一个数组lvalue;
a.m
,除非m
是枚举成员,或非静态成员函数,或a
是rvalue且m
是非引用类型的非静态数据成员;
p->m
,除非m
是枚举成员,或非静态成员函数;
a.*mp
,a
是一个lvalue,mp
是数据成员指针;
p->*mp
,mp
是数据成员指针;
逗号表达式,第二个运算数是lvalue;
条件运算符a ? b : c
,这里有很是复杂的规则,举其中一例,当b
和c
是相同类型的lvalue时;
字符串字面量;
显式转换为左值引用类型或函数的右值引用类型。
lvalue的性质:
与glvalue相同;
内置取地址运算符能够做用于lvalue;
可修改的lvalue能够出如今内置赋值运算符的左边;
能够用来初始化一个左值引用。
除字符串之外的字面量;
函数调用或重载运算符表达式,其返回类型为非引用类型;
内置算术运算、逻辑运算、比较运算、取地址运算符表达式;
a.m
或p->m
,m
是枚举成员或非静态成员函数(见下);
a.*mp
或p->*mp
,mp
是成员函数指针;
逗号表达式,第二个运算数是rvalue;
条件运算符a ? b : c
的部分状况,如b
和c
是相同类型的prvalue;
显式转换为非引用类型;
this
指针;
枚举成员;
非类型模板参数,除非它是左值引用类型;
lambda表达式。
prvalue的性质:
与rvalue相同;
不能是多态的;
非类类型且非数组的prvalue没有cv修饰符,即便写了也没有;
必须是彻底类型;
不能是抽象类型或其数组。
函数调用或重载运算符表达式,其返回类型为右值引用类型;
内置数组下标表达式a[n]
,a
是一个数组rvalue;
a.m
,a
是rvalue且m
是非引用类型的非静态数据成员;
a.*mp
,a
是一个rvalue,mp
是数据成员指针;
条件运算符a ? b : c
的部分状况,如b
和c
是相同类型的xvalue。
xvalue的性质;
与rvalue相同;
与glvalue相同。
glvalue的性质:
能够隐式转换为prvalue;
能够是多态的;
能够是不彻底类型。
rvalue的性质:
内置取地址运算符不能做用于rvalue;
不能出如今内置赋值或复合赋值运算符的左边;
能够绑定给const
左值引用(见下);
能够用来初始化右值引用(见下);
若是一个函数有右值引用参数和const
左值引用参数两个重载,传入一个rvalue时,右值引用的那个重载被调用。
还有一些特殊的分类:
对于非静态成员函数mf
及其指针pmf
,a.mf
、p->mf
、a.*pmf
和p->*pmf
都被归类为prvalue,但它们不是常规的prvalue,而是pending(即将发生的) member function call,只能用于函数调用;
返回void
的函数调用、向void
的类型装换和throw
语句都是void
表达式,不能用于初始化引用或函数参数;
C++中最小的寻址单位是字节,所以位域不能绑定到非const
左值引用上;const
左值引用和右值引用能够绑定位域,它们指向的是位域的一个拷贝。
终于把5个类别介绍完了。表达式能够分为lvalue、xvalue和prvalue三类,lvalue和prvalue与C++98中的lvalue和rvalue相似,而xvalue则彻底是为右值引用而生,兼有glvalue与rvalue的性质。除了这种三分类法外,表达式还能够分为lvalue和rvalue两类,它们之间的主要差异在因而否能够取地址;还能够分为glvalue和prvalue两类,它们之间的主要差异在因而否存在实体,glvalue有实体,于是能够修改原对象,xvalue常被压榨剩余价值。
咱们稍微岔开一会,来看两个与表达式分类相关的特性。
引用绑定有如下类型:
左值引用绑定lvalue,cv修饰符只能多不能少;
右值引用能够绑定rvalue,咱们一般不给右值引用加cv修饰符;
const
左值引用能够绑定rvalue。
左值引用绑定lvalue天经地义,没什么须要关照的。但rvalue都是临时对象,绑定给引用就意味着要继续用它,它的生命周期会受到影响。一般,rvalue的生命周期会延长到绑定引用的声明周期,但有如下例外:
由return
语句返回的临时对象在return
语句结束后即销毁,这样的函数老是会返回一个空悬引用(dangling reference);
绑定到初始化列表中的引用的临时对象的生命周期只延长到构造函数结束——这是个缺陷,在C++14中被修复;
绑定到函数参数的临时对象的生命周期延长到函数调用所在表达式结束,把该参数做为引用返回会获得空悬引用;
绑定到new
表达式中的引用的临时对象的生命周期只延长到包含new
的表达式的结束,不会跟着那个对象。
简而言之,临时变量的生命周期只能延长一次。
#include <utility> int&& rvalue_reference() { int local = 1; return std::move(local); } const int& const_lvalue_reference(const int& arg) { return arg; } int main() { auto&& i = rvalue_reference(); // dangling reference auto&& j = const_lvalue_reference(2); // dangling reference int k = 3; auto&& l = const_lvalue_reference(k); }
rvalue_reference
返回一个指向局部变量的引用,所以i
是空悬引用;2
绑定到const_lvalue_reference
的参数arg
上,函数返回后延长的生命周期达到终点,所以j
也是悬空引用;k
在传参的过程当中根本没有临时对象建立出来,因此l
不是空悬引用,它是指向k
的const
左值引用。
从C++11开始,auto
关键字用于自动推导类型,用的是模板参数推导的规则:若是是拷贝列表初始化,则对应模板参数为std::initializer_list<T>
,不然把auto
替换为T
。至于详细的模板参数推导规则,要介绍的话未免喧宾夺主了。
还好,这不是咱们的重点。在引出重点以前,咱们还得先看decltype
。
decltype
用于声明一个类型("declare type"),有两种语法:
decltype(entity)
;
decltype(expression)
。
第一种,decltype
的参数是没有括号包裹的标识符或类成员,则decltype
产生该实体的类型;若是是结构化绑定,则产生被引类型。
第二种,decltype
的参数是不能匹配第一种的任何表达式,其类型为T
,则根据其表达式类别讨论:
若是是xvalue,产生T&&
——#define is_xvalue(x) std::is_rvalue_reference<decltype((x))>::value
;
若是是lvalue,产生T&
——#define is_lvalue(x) std::is_lvalue_reference<decltype((x))>::value
;
若是是prvalue,产生T
——#define is_prvalue(x) !std::is_reference<decltype((x))>::value
。
所以,decltype(x)
和decltype((x))
产生的类型一般是不一样的。
对于不带引用修饰的auto
,初始化器的表达式类别会被抹去,为此C++14引入了新语法decltype(auto)
,产生的类型为decltype(expr)
,其中expr
为初始化器。对于局部变量,等号右边加上一对圆括号,能够保留表达式类别。
#include <utility> #include <type_traits> int non_reference() { return 1; } int& lvalue_reference() { static int i; return i; } const int& const_lvalue_reference() { return lvalue_reference(); } int&& rvalue_reference() { static int i; return std::move(i); } int main() { auto [s1, s2] = std::pair(2, 3); auto&& t1 = s1; static_assert(!std::is_reference<decltype(s1)>::value); static_assert(std::is_lvalue_reference<decltype(t1)>::value); int i1 = 4; auto i2 = i1; decltype(auto) i3 = i1; decltype(auto) i4{i1}; decltype(auto) i5 = (i1); static_assert(!std::is_reference<decltype(i2)>::value); static_assert(!std::is_reference<decltype(i3)>::value); static_assert(!std::is_reference<decltype(i4)>::value); static_assert(std::is_lvalue_reference<decltype(i5)>::value); auto n1 = non_reference(); decltype(auto) n2 = non_reference(); auto&& n3 = non_reference(); static_assert(!std::is_reference<decltype(n1)>::value, ""); static_assert(!std::is_reference<decltype(n2)>::value, ""); static_assert(std::is_rvalue_reference<decltype(n3)>::value, ""); auto l1 = lvalue_reference(); decltype(auto) l2 = lvalue_reference(); auto&& l3 = lvalue_reference(); static_assert(!std::is_reference<decltype(l1)>::value, ""); static_assert(std::is_lvalue_reference<decltype(l2)>::value, ""); static_assert(std::is_lvalue_reference<decltype(l3)>::value, ""); auto c1 = const_lvalue_reference(); decltype(auto) c2 = const_lvalue_reference(); auto&& c3 = const_lvalue_reference(); static_assert(!std::is_reference<decltype(c1)>::value, ""); static_assert(std::is_lvalue_reference<decltype(c2)>::value, ""); static_assert(std::is_lvalue_reference<decltype(c3)>::value, ""); auto r1 = rvalue_reference(); decltype(auto) r2 = rvalue_reference(); auto&& r3 = rvalue_reference(); static_assert(!std::is_reference<decltype(r1)>::value, ""); static_assert(std::is_rvalue_reference<decltype(r2)>::value, ""); static_assert(std::is_rvalue_reference<decltype(r3)>::value, ""); }
用auto
定义的变量都是int
类型,不管函数的返回类型的引用和const
修饰;用decltype(auto)
定义的变量的类型与函数返回类型相同;auto&&
是转发引用,n3
类型为int&&
,其他与decltype(auto)
相同。
众所周知,编译器常会执行NRVO(named return value optimization),减小一次对函数返回值的移动或拷贝。不过,这属于C++标准说编译器能够作的行为,却没有保证编译器会这么作,所以客户不能对此做出假设,从而须要提供一个拷贝或移动构造函数,尽管它们可能不会被调用。然而,并非全部状况下都能提供移动构造函数,即便能移动构造函数也未必只是一个指针的交换。总之,咱们明知移动构造函数不会被调用却还要硬着头皮提供一个,这样作很是形式主义。
因此,C++17规定了拷贝省略,确保在如下状况下,即便拷贝或移动构造函数有可观察的效果,它们也不会被调用,本来要拷贝或移动的对象直接在目标位置构造:
在return
表达式中,运算数是忽略cv修饰符之后的返回类型的prvalue;
在初始化中,初始化器是与变量相同类型的prvalue。
值得一提的是,这类行为在C++17中不能算是一种优化,由于不存在用来拷贝或移动的临时对象。事实上,C++17从新定义了表达式类别:
glvalue的求值能肯定对象、位域、函数的身份;
prvalue的求值初始化对象或位域,或计算运算数的值,由上下文决定;
xvalue是表示一个对象或位域的资源能被重用的glvalue;
lvalue是否是xvalue的glvalue;
rvalue是prvalue或xvalue。
这个定义在功能上与C++11中的相同,可是更清晰地指出了glvalue和prvalue的区别——glvalue产生地址,prvalue执行初始化。
prvalue初始化的对象由上下文决定:在拷贝省略的情形下,prvalue未曾有关联的对象;其余情形下,prvalue将产生一个临时对象,这个过程称为临时实体化(temporary materialization)。
临时实体化把一个彻底类型的prvalue转换成xvalue,在如下情形中发生:
把引用绑定到prvalue上;
类prvalue被获取成员;
数组prvalue被转换为指针或下标取元素;
prvalue出如今大括号初始化列表中,用于初始化一个std::initializer_list<T>
;
被使用typeid
或sizeof
运算符;
在语句expr;
中或被转换成void
,即该表达式的值被丢弃。
或者能够理解为,全部非拷贝省略的场合中的prvalue都会被临时实体化。
class NonMoveable { public: int i = 1; NonMoveable(int i) : i(i) { } NonMoveable(NonMoveable&&) = delete; }; NonMoveable make(int i) { return NonMoveable{i}; } void take(NonMoveable nm) { return static_cast<void>(nm); } int main() { auto nm = make(2); auto nm2 = NonMoveable{make(3)}; // take(nm); take(make(4)); take(NonMoveable{make(5)}); }
NonMoveable
的移动构造函数被声明为delete
,因而拷贝构造函数也被隐式delete
。在auto nm = make(2);
中,NonMoveable{i}
为prvalue,根据拷贝省略的第一条规则,它直接构造为返回值;返回值是NonMoveable
的prvalue,与nm
类型相同,根据第二条规则,这个prvalue直接在nm
的位置上构造;两部分结合,该声明式至关于NonMoveable nm{2};
。
在MSVC中,这段代码不能经过编译,这是编译器未能严格遵照C++标准的缘故。然而,若是在NonMoveable
的移动构造函数中添加输出语句,程序运行起来也没有任何输出,即便在Debug模式下、即便用C++11标准编译都如此。这也侧面反映出拷贝省略的意义。
C++11规定每一个表达式都属于lvalue、xvalue和prvalue三个类别之一,表达式另可分为lvalue和rvalue,或glvalue和prvalue。返回右值引用的函数调用是xvalue,右值引用类型的变量是lvalue。
const
左值引用和右值引用能够绑定临时对象,可是临时对象的声明周期只能延长一次,返回一个指向局部变量的右值引用也会致使空悬引用。
标识符加上一对圆括号成为表达式,decltype
用于表达式能够根据其类别产生相应的类型,用decltype(auto)
声明变量能够保留表达式类别。
C++17中prvalue是否有关联对象由上下文决定,拷贝省略规定了特定状况下对象不经拷贝或移动直接构造,NRVO成为强制性标准,使不能被移动的对象在语义上能够值传递。