C++左值&右值,左值引用&右值引用

Share on:
200 Words | Read in about 1 Min | View times

Overview

C++11引入了右值引用的概念,用以区分C++11之前的引用(统称左值引用)。那么什么是右值引用?右值又是什么?和左值、左值引用有什么关系和区别?本节内容将一一解答。

本系列文章将包括以下领域:

本章其他内容请见 《现代C++》

expression表达式

要搞清楚左值和右值的概念,首先需要了解表达式的概念。

表达式,expression,是指由运算符(operator)和运算对象(operand)构成的计算式。字面值(literal)和变量(variable)是最基本的表达式,函数返回值也是表达式。表达式是可以求值的,对表达式求值将得到一个结果,这个结果包含两个属性——类型(type)和值类别(value categoties)。

在现代C++中,一个表达式的值类型可以划分为:泛左值(glvalue)和右值(rvalue)。泛左值又可以划分为左值(lvalue)和将亡值(xvalue),右值又可以划分为将亡值(xvalue)和纯右值(prvalue)。它们的关系可以用一张图来表示:

表达式分类

glvalue = lvalue + xvalue

rvalue = prvalue + xvalue

你肯定一脸疑惑,我们接下来逐一展开细说。对于左值、将亡值、纯右值其实只是表达式的属性,主要体现在使用上,比如能否取地址、能否使用移动语义等。

lvalue左值

lvalue名为左值,是locator value的缩写,并不是left value,这点要特别强调。所以从字面意思也能看出,lvalue是表示对象位置之物,在表达式结束后依然持有对象地址,可以起到定位的作用。

  • 左值最重要的特点是可以取地址,这是区分右值的唯一标志。

  • 可修改的左值可以是赋值操作符的左操作数,但不是绝对,等号左边的值不一定是左值。

  • 所有具名对象或变量都是左值。

常见的左值表达式有以下类型:

  • 具名变量、函数指针,如std::cinstd::cout

  • 返回左值引用的函数调用

  • 赋值或组合赋值类型,如a = ba += b

  • 前置自增或前置自减,如++i--i

  • 解引用表达式,如*p

  • 逗号表达式的最右的值是左值,如a, b,其中b是左值

  • 条件表达式,如a ? b : c,其中bc是左值

  • 字符串字面值:"Hello World"

  • 引用的强制转型表达式:static_cast<int&>(x);

  • 右值引用强转函数类型:staic_cast<void(&&)(int)>(x);

总之,左值一定可以取地址,有名字的变量一定是左值(即使是void foo(T&& x)中的x也是左值,因为右值引用是有名字的,故右值引用也是左值)。等号左边的不一定是左值,因为=运算符是可以重载的,完全可以使等号左边重载为右值的形式。

prvalue纯右值

prvaluepure rvalue的缩写,纯粹的字面值(如25true),或者求值结果是字面值或不具名的临时对象时,是一个纯右值。

  • 纯右值不能是多态类型

  • class的类型和非数组的纯右值不能被constvolatile修饰。

  • 纯右值不能是不完整类型

常见的纯右值表达式有以下类型:

  • 除字符串字面值外的其他字面值量,如nullptrtruefalse

  • 返回非引用类型的函数调用

  • 后置自增或后置自减,如i++i--

  • 算数表达式,如a + ba % ba & b

  • 逻辑表达式,如a && b~a

  • 比较表达式,如a < ba >= b

  • 取址表达式,如&a,其结果相当于一个unsigned int类型的字面值

  • 逗号表达式的最右的值是右值,如a, b,其中b是右值

  • this指针,因为this也是一个地址

  • 非引用的强制转型表达式:static_cast<int>(x);(int)42

  • lambda表达式:[](int x) { return x * x; };

xvalue将亡值

在C++11之前,右值和C++11的纯右值是等价的;而将亡值是伴随右值引用加入的概念。将亡值是由右值引用产生而引起的。

用左值去初始化或赋值给一个对象时,会调用拷贝构造函数或拷贝赋值运算符;而用右值去初始化或赋值给一个对象时,会调用移动构造函数或移动赋值运算符。当该右值完成初始化或赋值后,它的资源已经被移动给了被初始化或被赋值的对象上,该右值马上会被析构销毁,即“将亡”。

常见的将亡值表达式有以下类型:

  • 返回右值引用的函数调用,如std::move(x)

  • 转换为右值引用的转换函数调用,如static_cast<T&&>(x)

以上两种情况的函数调用返回值都是不具名的右值引用,它们属于右值,又因为都与C++11新加入的移动语义相关,故被这类右值称为“将亡值”。xvalueprvalue在功能上及其类似,都不能作作为操作符的左操作数,都可以使用移动构造和移动赋值。

具名的右值引用是左值,不具名的右值引用是右值。

glvalue泛左值

泛左值要么是左值,要么是将亡值。具有以下特点:

  • glvalue可以隐式地转换为纯右值:lvalue->rvalue的隐式转换、数组到指针的隐式转换、函数到指针的隐式转换

  • glvalue可以是多态类型的

  • glvalue可以是不完整类型

rvalue右值

右值要么是纯右值,要么是将亡值。rvalue,是read value的缩写,并不是right value,这点要特别强调。具有以下特点:

  • rvalue不可取地址

  • rvalue不可用于赋值运算符或组合赋值的左操作数

  • rvalue可以用来初始化const左值引用,这样rvalue的生命周期将被延长至该作用域结束

  • rvalue可以用来初始化一个右值引用,这样rvalue的生命周起也被延长至该作用域结束

  • 如果存在一个函数的两个重载(只有某个参数不同,且这个参数在一个重载中是右值引用,另一个是const左值引用),此时用右值调用这个重载函数,将会匹配右值引用的函数

特殊情况

  • void表达式,函数返回值是void、强制类型转化为voidthrow异常都被归类为纯右值,但是它们不可以用来初始化const左值引用或右值引用,也不能作用函数的参数。

  • 位域(bit field)是左值,但是位域不能取地址,也不能将一个非const引用绑定到位域上。但是const引用可以绑定到位域上,此时会产生一个临时变量。

左值引用和右值引用

顾名思义,左值引用,是只能用来绑定左值的引用;右值引用,是只能用来绑定右值的引用。但const左值引用既可以绑定非const左值,又可以绑定const左值,还可以绑定右值。

右值引用往往是将不具名的变量取别名,这样一来该右值引用具名了,变成了一个左值。所以右值引用不一定是右值,也可以是左值。请看以下例子:

1foo(std::move(x)); //std::move的返回值是一个右值引用,因为不具名,所以是个右值
2
3int&& temp = std::move(x); //std::move的返回值是一个右值引用,但该右值引用有名字temp,所以是一个左值

右值引用是用来支持转移语义的。转移语义可以将资源如堆、系统对象等从一个对象转移到另一个对象,这样能够减少不必要的临时对象的创建、拷贝以及销毁,能够大幅度提高C++应用程序的性能。在后面的章节中我们将进一步讨论C++的移动语义

Prev Post: 『C++ new表达式、operator new和placement new』
Next Post: 『C++ volatile的作用』