c++ value categories

c++ value categories

问题源起

不知道各位会不会有过这样的疑问,左值是什么?右值是什么?左值引用是什么?右值引用又是什么?

这里我可以给大家说一下我的心路历程:

最开始的理解:左,右为赋值符号的左、右两侧的值,也就是

auto a = 10;
// 这里a为左值, 10为右值

// 这里会很奇怪??? 
auto b = a;
// 按照左、右之分的原则,那么这里b为左值,a为右值
// 这里a又是左值,又是右值,所以这样的说法肯定是不对的。

第二次理解:左值为可以取地址的值,而右值为不可以取地址的值

auto a = 10;
// 这里a为左值, &a有效,这里10为右值,对常量取址不可操作

auto b = a;
// 这里a为左值,因为&a有效,同时b也为左值,因为&b也有效

T GetT();
// GetT()的返回值为右值,因为&GetT()不可操作

// 使用右值引用获取临时变量的地址[延迟生命周期]
T&& r_ref = GetT();
// &r_ref为左值,因为&r_ref有效

// 但是通过汇编我们能发现,其实GetT()的rax[返回值寄存器]和r_ref指的内存是同一个地址的。所以这样来描述左右值也是不合理

这里第二次理解,可以通过汇编来理解这里的逻辑。

在这里插入图片描述

解释了左右值之后,我们再来理解左值引用,右值引用

我对这里的最开始的理解是:
左值引用是对左值的引用[也只能对左值],右值引用时对右值的引用[也只能对右值],即

int a = 10;
int& b = a; // 左值引用
int&& c = 10; // 右值引用

T GetT();
T&& d = GetT(); // 右值引用

T& GetT();
T& e = GetT(); // 左值引用

const int& f = 10; // 常量引用

本文目的

本文的目的是为了搞定c++语言本身的Value categories【值类别】,要注意的是,c++语言对于任意一个表达式而言(字面量/变量名…)都有两个独立的属性:(1)type(类型)(2)value categories(值类别),不知道读者会不会存在一个很常见的问题:右值引用是左值(英文术语:lvalue)还是右值(英文术语:rvalue)。这里的答案是右值引用是一个左值(lvalue),其实大部分读者有这个疑惑的原因就是没有把“一个表达式的两个独立属性 type 和 value categories”分开来。右值引用描述的是type,由于其经常用于做临时空间的利用(可以理解为指针延长了临时变量的生命周期)其本质是一个“引用”,其value categories是一个(lvalue)。

标准如何说?

在c++标准中value categories 存在哪些类型?
使用c++11 和 之前的标准来说:

c++语言使用 lvalue rvalue来表示。

在c++11引入移动语义和右值引用之后,加入了glvalue , xvalue,prvalue 等关键词。

lvalue rvalue 的评判标准是什么?在c++的历史过程中间分别表示了什么?

在cppref中间的解释来说

so-called, historically, because lvalues could appear on the left-hand side of an assignment expression

这样来使用lvalue这个说法本质上是因为历史上c++11之前,是使用lvalue 来说明是在= 的左边的表达式

而rvalue自然就是= 后的表达式而言。

但是在c++11 的标准之后,这类说法已经不再成立了。

c++11 后对lvalue 的定义是:

have identity and cannot be moved from are called lvalue expressions;

拥有identity ,同时不能被moveed 的表达式(后面会详细说明identity 是什么c)

所以lvalue 的定义在c++的历史中已经发生了变化,笔者也不希望读者再次使用这一套标准来理解。因为真的非常麻烦。

c++11后对rvalue 的定义是:

xvalue + prvalue等于rvalue

can be moved

所以rvalue 的定义关注的是是否能被move (移动语义)

c++ 11 后value categories 分类

c++11 标准中任何一个表达式都属于prvalue , xvalue ,lvalue 三个原子类型之一

c++11 value categories identity moved
prvalue no yes
xvalue yes yes
lvalue yes no

c++11 中还存在其他两个混合类型,他们的组成和标准如下:

c++11 mixed value categories 组成1 组成2 选择标准
glvalue lvalue xvalue 拥有identity的表达式
rvalue xvalue prvalue 能被moved表达式

如何理解identity

cppref定义

it’s possible to determine whether the expression refers to the same entity as another expression, such as by comparing addresses of the objects or the functions they identify (obtained directly or indirectly)

https://stackoverflow.com/questions/53443234/whats-the-meaning-of-identity-in-the-definition-of-value-categories-in-c

cppref定义的是,identity意味着可以有多个表达式来关联引用同一个实体,同时他们还可以使用地址或者其他方式来比较…

初步理解

identity的理解就是,能在同一个地址,同一个时间确定唯一的一个实体的,所以这里会有几个定义

  • 表达式中间的名字(字符串序列,说白了变量名)都是一个lvalue
  • 由于一个地址一个时间下面不可能存在相同类型的两个对象。所以指针的解指针指向的往往是一个左值,同时引用也一定是一个左值。
  • 由于引用一定确定一个实体,所以所有返回引用的函数都是泛左值(返回lvalue的引用是lvalue,返回右值引用的函数是xvalue)
从生命周期和未来使用来思考

从上面来理解,我相信读者会和笔者一样感觉到一脸懵逼,所以笔者这里换一个思路来理解:
identify本质可以理解是否编译器一定需要为其生成唯一的内存地址。或者换一个角度来说是未来是否还需要使用到其地址

这里我们从prvalue 来进行理解,因为prvalue 是唯一一个没有identity的原子类型

prvalue 一般类型有:

字面量(42, true, nullptr)
非引用的函数返回值(立即对象,临时对象)
a++, a--等内建的前置自增和自减(临时对象)
a+b, a-b等内建运算符(临时对象)
&a取地址运算符(本质是一个数,相当于函数的临时对象值)
a.m (Pending member function call)[特殊类型,同时这玩意还不能用于初始化右值引用](待研究)
a->m(Pending member function call)[特殊类型,同时这玩意还不能用于初始化右值引用](待研究)
a.*m (Pending member function call)[特殊类型,同时这玩意还不能用于初始化右值引用](待研究)
a->*m(Pending member function call)[特殊类型,同时这玩意还不能用于初始化右值引用](待研究)
this指针(this指针本质是参数,被对象地址替换)
枚举类型(编译期替换,不存)
lambda表达式(临时对象)
requires表达式(暂时没研究)

可以看到prvalue 的类型中,除了笔者本身不了解的成员函数类型和成员函数指针类型之外,其他的类型都可以使用临时值和编译器替换量来理解,prvalue 类型可以理解为马上会销毁的对象,或者压根不是对象的内容。由于临时变量编译器可以自己选择是否为其生成地址,以及临时变量在使用过后会马上会销毁,所以没有identity 可以理解为编译器不一定为其生成地址(在不需要的时候)这个时候未来也不可能需要用到这个临时对象的地址(生命周期结束了)。

可以使用xvalue prvalue 之间的差距,来进一步说明identity 的意义

xvalue 一般类型有

强转成右值引用(static_cast<T&&>, std::move)
临时对象被使用了对象内部数据成员(a.m)[a : rvalue]

这两者综合过程被解释为 "临时对象物化" temporary materialization.

当临时对象物化之后意味着我们需要延长这个临时对象的生命周期,编译器自然也需要为其生成唯一的地址,以便后面被其他程序指令所使用,甚至所写入。

所以identity 可以初步理解为编译器是否一定为其生成了唯一地址,方便后续程序继续使用它。

如何理解moved

moved的定义非常简单,就是能被移动语义所识别。

rvalue的目的是什么?

从前面得知,rvalue的特性是can moved ,而此特性本质是为了更好使用移动语义,移动语义的目的是为了更好的资源复用。所以rvalue 的目的是:为了语言能更好的复用可复用对象,能减少开销。

rvalue如何实现moved

rvalue 实现moved 的方式是通过右值引用来实现的,通过rvalue 来初始化右值引用,可实现右值引用延长此rvalue生命周期的效果。从而实现资源的复用。

用一个简单的例子来看,使用move前后的差距

struct T {
    
    
    int a;
    int b;
    int c;
};

T GetT() {
    
    
    // ...
}

// no moved
void Test1() {
    
    
    T t1 = GetT();
    // 内部细节;
    // Test1栈帧
    // 申请临时对象Tmp(T)
    // 将Tmp地址作为参数传入GetT()函数中
    // GetT()中在Tmp地址上初始化
    // 申请变量t1
    // 拷贝Tmp -> t1
    // Tmp地址销毁对象
}

// moved
void Test2() {
    
    
    T&& t1 = GetT();
    // 内部细节;
    // Test2栈帧
    // 申请临时对象Tmp(T)
    // 将Tmp地址作为参数传入GetT()函数中
    // GetT()中在Tmp地址上初始化
    // t1指针指向Tmp,Tmp生命周期延长
}

所以xvalue本质是为了更好的实现移动语义能力的。

prvalue的目的

通过上一节,其实我们已经可以理解rvalue 的目的,rvalue 本身的目的是因为本身本来是可以被moved 的,而这个特性结合右值引用后,能将临时空间的生命周期进一步延长,从而延长生命周期。

那么为什么还要额外强调prvalue 呢。

让我们再次回顾到前一个例子

struct T {
    
    
    int a;
    int b;
    int c;
};

T GetT() {
    
    
    // ...
}

void test() {
    
    
    T t1 = GetT();
}

对于这样一个例子,由于我们没有使用右值引用来绑定GetT() 函数的返回值,所以这里还是会涉及到临时空间和真实变量t1 之间的拷贝。那这里有没有另外的办法来解决这个问题呢?

由于这里临时变量的生成除了做t1 变量的初始化之外其实没有任何意义,所以编译器的思路是,为什么不能直接把t1 的地址传入到GetT() 函数中呢?这样就没有临时空间和真实变量之间的拷贝了。因为临时空间的地址此时就是真实变量地址。

在这样的思路之后程序变成了下面的行为

void Test1() {
    
    
    T t1 = GetT();
    // 内部细节;
    // Test1栈帧
    // 申请变量t1
    // 将t1地址作为参数传入GetT()函数中
    // GetT()中在t1地址上初始化
}

这就是复制省略。

目前复制省略在c++17 中已经成为了语言标准,所以大家可以放心使用。

理解一些日常问题

右值引用是lvalue还是rvalue

这个问题的答案核心是,引用本身一定绑定了一个实体,即有一个编译器一定生成对象的地址被绑定了。同时引用本身不能被moved ,所以右值引用是左值

std::move后的临时对象是否会被复制省略

首先我们来理解std::move的本质是什么

return static_cast<T&& >()

std::move 本质是做了右值引用的强转,返回后的值属于xvalue ,而复制省略省略xvalue 所以复制省略会被拦截

猜你喜欢

转载自blog.csdn.net/gaoyanwangxin/article/details/128332886
今日推荐