从上一讲智能指针开始,我们已经或多或少接触了移动语义。本讲我们就完整地讨论一下移动语义和相关的概念。移动语义是 C++11 里引入的一个重要概念;理解这个概念,是理解很多现代 C++ 里的优化的基础。
值分左右
我们常常会说,C++ 里有左值和右值。这话不完全对。标准里的定义实际更复杂,规定了下面这些值类别(value categories):
我们先理解一下这些名词的字面含义:
一个 lvalue 是通常可以放在等号左边的表达式,左值
一个 rvalue 是通常只能放在等号右边的表达式,右值
一个 glvalue 是 generalized lvalue,广义左值
一个 xvalue 是 expiring lvalue,将亡值
一个 prvalue 是 pure rvalue,纯右值
还是有点晕,是吧?我们暂且抛开这些概念,只看其中两个:lvalue 和 prvalue。
左值 lvalue 是有标识符、可以取地址的表达式,最常见的情况有:
变量、函数或数据成员的名字
返回左值引用的表达式,如 ++x、x = 1、cout << ' '
字符串字面量如 "hello world"
在函数调用时,左值可以绑定到左值引用的参数,如 T&。一个常量只能绑定到常左值引用,如 const T&。
反之,纯右值 prvalue 是没有标识符、不可以取地址的表达式,一般也称之为“临时对象”。最常见的情况有:
返回非引用类型的表达式,如 x++、x + 1、make_shared<int>(42)
除字符串字面量之外的字面量,如 42、true
在 C++11 之前,右值可以绑定到常左值引用(const lvalue reference)的参数,如 const T&,但不可以绑定到非常左值引用(non-const lvalue reference),如 T&。从 C++11 开始,C++ 语言里多了一种引用类型——右值引用。右值引用的形式是 T&&,比左值引用多一个 & 符号。跟左值引用一样,我们可以使用 const 和 volatile 来进行修饰,但最常见的情况是,我们不会用 const 和 volatile 来修饰右值。本专栏就属于这种情况。
引入一种额外的引用类型当然增加了语言的复杂性,但也带来了很多优化的可能性。由于 C++ 有重载,我们就可以根据不同的引用类型,来选择不同的重载函数,来完成不同的行为。回想一下,在上一讲中,我们就利用了重载,让 smart_ptr 的构造函数可以有不同的行为:
template <typename U>
smart_ptr(const smart_ptr<U>& other) noexcept
{
ptr_ = other.ptr_;
if (ptr_) {
other.shared_count_->add_count();
shared_count_ =
other.shared_count_;
}
}
template <typename U>
smart_ptr(smart_ptr<U>&& other) noexcept
{
ptr_ = other.ptr_;
if (ptr_) {
shared_count_ =
other.shared_count_;
other.ptr_ = nullptr;
}
}
你可能会好奇,使用右值引用的第二个重载函数中的变量 other 算是左值还是右值呢?根据定义,other 是个变量的名字,变量有标识符、有地址,所以它还是一个左值——虽然它的类型是右值引用。
尤其重要的是,拿这个 other 去调用函数时,它匹配的也会是左值引用。也就是说,类型是右值引用的变量是一个左值!这点可能有点反直觉,但跟 C++ 的其他方面是一致的。毕竟对于一个右值引用的变量,你是可以取地址的,这点上它和左值完全一致。稍后我们再回到这个话题上来。
再看一下下面的代码:
smart_ptr<shape> ptr1{new circle()};
smart_ptr<shape> ptr2 = std::move(ptr1);
第一个表达式里的 new circle() 就是一个纯右值;但对于指针,我们通常使用值传递,并不关心它是左值还是右值。
第二个表达式里的 std::move(ptr) 就有趣点了。它的作用是把一个左值引用强制转换成一个右值引用,而并不改变其内容。从实用的角度,在我们这儿 std::move(ptr1) 等价于 static_cast<smart_ptr<shape>&&>(ptr1)。因此,std::move(ptr1) 的结果是指向 ptr1 的一个右值引用,这样构造 ptr2 时就会选择上面第二个重载。
我们可以把 std::move(ptr1) 看作是一个有名字的右值。为了跟无名的纯右值 prvalue 相区别,C++ 里目前就把这种表达式叫做 xvalue。跟左值