C++ 左值与右值
C++ 的开发或者学习中,一定不可避免的遇到左值和右值,左值引用和右值引用这 4 个名词,这类概念在 C 中并不重要,但是对于 C++ 来说确涉及到了许多问题特性。随着 C++11 标准的正式公布,对于值类型的理解变得重要起来。
C 中的左右值
在 C 中,左值指的是既能够出现在等号左边也能出现在等号右边的变量或表达式,右值指的则是只能出现在等号右边的变量或表达式。
通常来说有名字的变量和常量是左值,而由运算操作(加减乘除,返回等)所产生的中间结果就是右值。
C++ 中的左右值
在 C++ 中,左值大致与 C 中一样,可以大致由 2 点确定一个左值:有名字且可以取地址的对象。
C++11 以后,表达式按值类别分,必然属于以下三者之一:左值 (left value, lvalue),将亡值 (expiring value, xvalue),纯右值 (pure rvalue, pralue)。其中,左值和将亡值合称泛左值 (generalized lvalue, glvalue),纯右值和将亡值合称右值 (right value, rvalue)。
由于值类型是仅存在于语言中的概念,所以很难用简介的语言去定义,只能通过归纳的方法。
左值
- 具有名字的变量或者函数(注意,指针是变量,枚举不是变量)
- 具有名字的变量的成员(例如成员访问运算符表达式,下标运算符表达式)
- 返回在当前作用域满足第 1,2 条的表达式或者函数调用(例如前置自增自减运算,以及传入 x 的引用返回 x 的引用的函数调用)
- 具有名字的引用(左值引用)(注意,引用在语义上不是变量,是别名)
- 即使变量的类型是右值引用,由其名字构成的表达式仍是左值表达式,因为他满足第 1 条
- 对满足条件 1,2,3,4 的表达式的寻址表达式(注意,对引用的寻址实际上是对引用所指的对象寻址)
- 右侧对象满足 1,2,3,4 的逗号运算符表达式,左侧对象任意
- 后两个表达式满足 1,2,3,4 的三目运算符表达式(若仅有一个满足,则仅在进入该分支时为左值)
- 字符串字面值(其类型为 const char*)
- 返回类型是到函数的右值引用的函数调用表达式或重载的运算符表达式
- 转换为函数的右值引用类型的转型表达式(例如
static_cast<void (&&)(int)>(x)
) - 返回值同内建表达式的函数
纯右值
- 除了字符串字面量以外的字面量(例如 1,1u,1ll,nullptr,true)
- 返回值在当前作用域范围没有名字的函数调用或者表达式(例如后置自增,算术运算表达式,逻辑运算表达式,取地址表达式)
- 枚举项
- 内建逗号表达式,其中右侧运算符满足 1,2,3
- 后两个表达式满足 1,2,3 的三目运算符表达式(若仅有一个满足,则仅在进入该分支时为纯右值)
- 转换为非引用类型的转型表达式,例如
static_cast<double>(x)
、std::string{}
或(int)42
- this 指针
- lambda 表达式
- constexpr 修饰的变量或函数(若能被编译期求值)
将亡值
- 返回类型为右值引用的函数调用或者表达式(例如
std::move(x)
) - 下标表达式的操作数
- 后两个表达式满足 1 的三目运算符表达式
- 转换为对象的右值引用类型的转型表达式(例如
static_cast<char&&>(x)
) - 使用成员访问运算符的表达式,当左侧表达式为右值,且成员非静态
这三个概念虽然看起来并不重要,但是这三者有着不同的生命周期:
左值和将亡值的析构直到当前块结束,右值就地析构。
当右值被 std::move 转换为将亡值后,该值的生命周期会延长到调用 std::move 所在代码块的结束时。
根据上一条,禁止使用诸如 return std::move(x)
作为函数返回语句,因为 std::move 只能将生命周期延长到 std::move 所在代码块的结束,而不能将生命周期传递出去。
正确的使用方法是在外部调用 std::move:auto a = std::move(f(x));
。