时间:2021-05-20
目标
以下代码能否编译通过,能否按照期望运行?
C++98表达式类别
每个C++表达式都有一个类型:42的类型为int,int i;则(i)的类型为int&。这些类型落入若干类别中。在C++98/03中,每个表达式都是左值或右值。
左值(lvalue)是指向真实储存在内存或寄存器中的值的表达式。“l”指的是“left-hand side”,因为在C中只有lvalue才能写在赋值运算符的左边。相对地,右值(rvalue,“r”指的是“right-hand side”)只能出现在赋值运算符的右边。
有一些例外,如const int i;,i虽然是左值但不能出现在赋值运算符的左边。到了C++,类类型的rvalue却可以出现在赋值运算符的左边,事实上这里的赋值是对赋值运算符函数的调用,与基本类型的赋值是不同的。
lvalue可以理解为可取地址的值,变量、对指针解引用、对返回类型为引用类型的函数的调用等,都是lvalue。临时对象都是rvalue,包括字面量和返回类型为非引用类型的函数调用等。字符串字面量是个例外,它属于不可修改的左值。
赋值运算符左边需要一个lvalue,右边需要一个rvalue,如果给它一个lvalue,该lvalue会被隐式转换成rvalue。这个过程是理所当然的。
动机
C++11引入了右值引用和移动语义。函数返回的右值引用,顾名思义,应该表现得和右值一样,但是这会破坏很多既有的规则:
这给传统的lvalue/rvalue二分法带来了挑战,C++委员会面临选择:
上述问题只是冰山一角;历史选择了第三种方案。
C++11表达式类别
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包括:(点击展开)
lvalue的性质:
prvalue包括:
prvalue的性质:
xvalue包括:
xvalue的性质;
glvalue的性质:
rvalue的性质:
还有一些特殊的分类:
终于把5个类别介绍完了。表达式可以分为lvalue、xvalue和prvalue三类,lvalue和prvalue与C++98中的lvalue和rvalue类似,而xvalue则完全是为右值引用而生,兼有glvalue与rvalue的性质。除了这种三分类法外,表达式还可以分为lvalue和rvalue两类,它们之间的主要差别在于是否可以取地址;还可以分为glvalue和prvalue两类,它们之间的主要差别在于是否存在实体,glvalue有实体,因而可以修改原对象,xvalue常被压榨剩余价值。
引用绑定
我们稍微岔开一会,来看两个与表达式分类相关的特性。
引用绑定有以下类型:
左值引用绑定lvalue天经地义,没什么需要关照的。但rvalue都是临时对象,绑定给引用就意味着要继续用它,它的生命周期会受到影响。通常,rvalue的生命周期会延长到绑定引用的声明周期,但有以下例外:
简而言之,临时变量的生命周期只能延长一次。
#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左值引用。
auto与decltype
从C++11开始,auto关键字用于自动推导类型,用的是模板参数推导的规则:如果是拷贝列表初始化,则对应模板参数为std::initializer_list<T>,否则把auto替换为T。至于详细的模板参数推导规则,要介绍的话未免喧宾夺主了。
还好,这不是我们的重点。在引出重点之前,我们还得先看decltype。
decltype用于声明一个类型("declare type"),有两种语法:
第一种,decltype的参数是没有括号包裹的标识符或类成员,则decltype产生该实体的类型;如果是结构化绑定,则产生被引类型。
第二种,decltype的参数是不能匹配第一种的任何表达式,其类型为T,则根据其表达式类别讨论:
因此,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)相同。
C++17表达式类别
众所周知,编译器常会执行NRVO(named return value optimization),减少一次对函数返回值的移动或拷贝。不过,这属于C++标准说编译器可以做的行为,却没有保证编译器会这么做,因此客户不能对此作出假设,从而需要提供一个拷贝或移动构造函数,尽管它们可能不会被调用。然而,并不是所有情况下都能提供移动构造函数,即使能移动构造函数也未必只是一个指针的交换。总之,我们明知移动构造函数不会被调用却还要硬着头皮提供一个,这样做非常形式主义。
所以,C++17规定了拷贝省略,确保在以下情况下,即使拷贝或移动构造函数有可观察的效果,它们也不会被调用,原本要拷贝或移动的对象直接在目标位置构造:
值得一提的是,这类行为在C++17中不能算是一种优化,因为不存在用来拷贝或移动的临时对象。事实上,C++17重新定义了表达式类别:
这个定义在功能上与C++11中的相同,但是更清晰地指出了glvalue和prvalue的区别——glvalue产生地址,prvalue执行初始化。
prvalue初始化的对象由上下文决定:在拷贝省略的情形下,prvalue不曾有关联的对象;其他情形下,prvalue将产生一个临时对象,这个过程称为临时实体化(temporary materialization)。
临时实体化把一个完全类型的prvalue转换成xvalue,在以下情形中发生:
或者可以理解为,所有非拷贝省略的场合中的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成为强制性标准,使不能被移动的对象在语义上可以值传递。
参考
Value categories - cppreference.com
Value categories - [l, gl, x, r, pr]values
Value Categories in C++17
到此这篇关于C++98/11/17表达式类别的文章就介绍到这了,更多相关C++98/11/17表达式类别内容请搜索以前的文章或继续浏览下面的相关文章希望大家以后多多支持!
声明:本页内容来源网络,仅供用户参考;我单位不保证亦不表示资料全面及准确无误,也不保证亦不表示这些资料为最新信息,如因任何原因,本网内容或者用户因倚赖本网内容造成任何损失或损害,我单位将不会负任何法律责任。如涉及版权问题,请提交至online#300.cn邮箱联系删除。
条件表达式条件表达式也称为三元表达式,表达式的形式:xifCelsey。流程是:如果C为真,那么执行x,否则执行y。经过测试x,y,C可以是函数,表达式,常量等
c语言逗号表达式的运算规则c语言逗号表达式是由左向右进行的:k=3*2=6,K+2=8,表达式返回8。逗号表达式用法:当顺序点用,结合顺序是从左至右,用来顺序求
本文实例为大家分享了C语言实现对后缀表达式(逆波兰表达式)的求解代码,供大家参考,具体内容如下逆波兰表达式:逆波兰表达式又叫后缀表达式。它是由相应的语法树的后序
js中的正则表达式比起C#中的正则表达式要弱很多,但基本够用了1定义正则表达式2关于验证的三个这则表达式方法3正则表达式式的转义字符1定义正则表达式在js中定义
这是今天在温习lambda表达式的时候想到的问题,众所周知C系列语言中的三元运算符(?:)是一个非常好用的语句,关于C中的三元运算符表达式1?表达式2:表达式3