本文主要介绍 C++11 中值类别(value category)的概念、判定与应用,并关注 C++11 引入的右值引用(rvalue reference)与移动语义(move semantics),最后给出在编写类的成员函数时支持右值引用的示例。
关注这些问题的一个现实动机是在编写设计资源管理的类时,我们希望通过支持移动语义来提高部分场景下的初始化和赋值的效率。所谓移动语义是指某个对象 a 管理的资源可以被不经拷贝地移动给对象 b,同时对象 a 失去对该部分资源的控制。右值引用的出现为这一问题提供了自然的解决方案。
值类别:左值与右值
首先我们需要引入两个概念:
- 有名值:有名字/身份/地址的。
- 可移值:所控制的资源可以被移交所有权。例如对象 a 管理的资源可以被不经拷贝地移动给对象 b,同时对象 a 失去对该部分资源的控制(否则称为浅拷贝)。
C++11 中将值分为左值 lvalue 和右值 rvalue,其中右值细分为纯右值 prvalue 和将亡值 xvalue。
右值即可移值。若不可移交所有权,则必然是左值。由于无名不可移的值是无意义的,因此左值必然是有名的。而右值可以有名也可以无名。无名右值较为常见,称为纯右值;有名右值是比较特殊的存在,称为将亡值。
右值最字面的性质即它不能作为内置赋值操作的左操作数。而左值,一般既可以作为左操作数又可以作为右操作数。
总结一下五种类型:
- lvalue:有名不可移。
- xvalue:有名可移。
- prvalue:无名可移。
- glvalue:有名。
- rvalue:可移。
它们之间的集合关系如图所示:
先看比较特殊的 xvalue 将亡值。这厮精神可嘉,有名头又甘愿牺牲,定非我等凡人能及。简单来说,只有返回右值引用的函数返回值是 xvalue,或者 xvalue 的部分(如数组元素,对象成员,严格来说还需要满足一定条件)是 xvalue。
其余的右值则自动归入了 prvalue 纯右值的大熔炉。纯右值虽多杂,但主要不出两类:除 string 外的字面量(literal)和枚举值、非引用的函数调用返回值。除此之外,普通成员函数本身也是 prvalue。
最后讨论 lvalue 左值。左值可标识但不可转移。它可以出现在赋值语句的左边,通常有高级语言意义上的名字,可以取地址,生命周期为所在的 scope。注意在字面量(literal)中,只有 string literal 是 lvalue,其余都是 prvalue。
对于左值的引用就是左值引用,而对于右值的引用就是右值引用。左值引用只能由左值初始化,右值引用只能由右值初始化。
值语义和引用语义
对于具有值语义的类型,拷贝一个对象会得到一个新对象,且这两个对象不再具有相关性。也就是说,修改其中一个,对另一个不可见。大多数 C++ 基本类型和标准库类型都具有值语义。
对于具有引用语义(对象语义)的类型,则会保持相关性。修改其中一个,另一个也会跟着修改。事实上在大多数情况下这种拷贝是被禁止的。
注意,如果考虑指向哪个对象,则 C++ 指针具有值语义;如果讨论指向的对象本身,那么 C++ 指针又具有引用语义。
相对引用语义而言,值语义的生命周期管理非常简单。值语义的对象要么是栈对象,要么是其他对象的成员。
移动语义:右值引用与std::move
对于具有值语义的类型,我们通常会使其支持深拷贝,即在拷贝操作或复制构造时,为目标对象分配新的资源空间并复制数据。但很多时候仅仅支持深拷贝是有缺陷的,容易导致不必要的内存分配回收和复制操作。
为此,我们引入移动语义。移动语义使一个类型的对象可以被移动到另一个对象。所谓移动是指对象持有的资源和数据被转移给了另一个对象,而被移动对象则处于有效但未定义的状态。请注意与浅拷贝的不同之处。
std::move 等同于类型转换 static_cast<T&&>(x)
,它的作用是将任意引用强制转换为右值引用。
例如一个不需再使用的非临时对象,我们希望将它作为参数传给某个具有非引用形参的函数。此时可以通过 std::move 将它强制标记成一个临时对象(右值),进而使得传参时发生的是移动构造而非复制构造,以节省开销。
稍作修改,我们也可以实现可移动但不可拷贝的类型。
示例程序:支持深拷贝和移动语义的类
#include <utility>
struct Array
{
int *data;
Array(): data(new int[5])
{
}
~Array()
{
delete[] data;
}
Array(const Array &d): data(new int[5])
{
for(int i=0;i<5;i++)
data[i] = d.data[i];
}
Array& operator=(const Array &d)
{
for(int i=0;i<5;i++)
data[i] = d.data[i];
return *this;
}
Array(Array&& d) noexcept: data(d.data)
{
d.data = nullptr;
}
Array& operator =(Array&& d) noexcept
{
std::swap(data, d.data);
return *this;
}
};
void fun(Array a)
{
}
int main()
{
Array a;
Array b(a); // const& copy
Array c(std::move(a)); // && nocopy
fun(c); // const& copy
fun(Array()); // && nocopy
fun(std::move(c)); // && nocopy
}
关于生育率断崖抖个机灵:人尚可有复制语义,但至少目前,人生只能有移动语义。当繁衍意味着母体的凋零,异化自难免。希望一个好时代,在有生之年可以看到。