Skip to content

Latest commit

 

History

History
164 lines (119 loc) · 7.63 KB

File metadata and controls

164 lines (119 loc) · 7.63 KB

A.1 右值引用

如果从事过C++编程,就会对引用比较熟悉,引用允许为已经存在的对象创建一个新的名字。对新引用所做的访问和修改操作,都会影响它的原型。

例如:

int var=42;
int& ref=var;  // 创建一个var的引用
ref=99;
assert(var==99);  // 原型的值被改变了,因为引用被赋值了

目前为止,我们用过的所有引用都是左值引用(C++11之前)——对左值的引用。lvalue指的是可以放在赋值表达式左边的事物——在栈上或堆上分配的命名对象,或者其他对象成员——有明确的内存地址。rvalue指的是可以出现在赋值表达式右侧的对象——例如,文字常量和临时变量。因此,左值引用只能被绑定在左值上,而不是右值。

不能这样写:

int& i=42;  // 编译失败

例如,因为42是一个右值。你可能通常使用下面的方式讲一个右值绑定到一个const左值引用上:

int const& i = 42;

这算是钻了标准的一个空子吧。不过,这种情况我们之前也介绍过,我们通过对左值的const引用创建临时性对象,作为参数传递给函数。其允许隐式转换,所以你可这样写:

void print(std::string const& s);
print("hello");  //创建了临时std::string对象

C++11标准介绍了右值引用(rvalue reference),这种方式只能绑定右值,不能绑定左值,其通过两个&&来进行声明:

int&& i=42;
int j=42;
int&& k=j;  // 编译失败

因此,可以使用函数重载的方式来确定:函数有左值或右值为参数的时候,看是否能被同名且对应参数为左值或右值引用的函数所重载。其基础就是C++11新添语义——移动语义(move semantics)。

A.1.1 移动语义

右值通常都是临时的,可以随意修改。如果知道函数的某个参数是一个右值,就可以将其看作为一个临时存储,不影响程序的正确性。比起拷贝右值参数的内容,不如移动其内容。动态数组比较大时,能节省很多内存,提供更多的优化空间。试想,一个函数以std::vector<int>作为一个参数,就需要将其拷贝进来,而不对原始的数据做任何操作。C++03/98的办法是,将这个参数作为一个左值的const引用传入,然后做内部拷贝:

void process_copy(std::vector<int> const& vec_)
{
  std::vector<int> vec(vec_);
  vec.push_back(42);
}

这就允许函数能以左值或右值的形式进行传递,不过任何情况下都是通过拷贝来完成的。如果使用右值引用版本的函数来重载这个函数,就能避免在传入右值的时,进行内部拷贝,从而可以对原始值进行任意的修改:

void process_copy(std::vector<int> && vec)
{
  vec.push_back(42);
}

如果这个问题存在于类的构造函数中,内部右值会在新的实例中使用。可以参考一下代码中的例子(默认构造函数会分配很大一块内存,在析构函数中释放)。

代码A.1 使用移动构造函数的类

class X
{
private:
  int* data;

public:
  X():
    data(new int[1000000])
  {}

  ~X()
  {
    delete [] data;
  }

  X(const X& other):  // 1
   data(new int[1000000])
  {
    std::copy(other.data,other.data+1000000,data);
  }
  
  X(X&& other):  // 2
    data(other.data)
  {
    other.data=nullptr;
  }
};

一般情况下,拷贝构造函数①都是这么定义:分配一块新内存,然后将数据拷贝进去。不过,现在有了一个新的构造函数,可以接受右值引用来获取老数据②,就是移动构造函数。在这个例子中,只是将指针拷贝到数据中,将other以空指针的形式留在了新实例中。使用右值里创建变量,就能避免了空间和时间上的多余消耗。

X类(代码A.1)中的移动构造函数,仅作为一次优化。在其他例子中,有些类型的构造函数只支持移动构造函数,而不支持拷贝构造函数。例如,智能指针std::unique_ptr<>的非空实例中,只允许这个指针指向其对象,所以拷贝函数在这里就不能用了(如果使用拷贝函数,就会有两个std::unique_ptr<>指向该对象,不满足std::unique_ptr<>定义)。不过,移动构造函数允许对指针的所有权进行传递,并且允许std::unique_ptr<>像一个带有返回值的函数一样使用——指针的转移是通过移动,而非拷贝。

如果你已经知道,某个变量在之后就不会在用到了,这时候可以选择显式的移动,你可以使用static_cast<X&&>将对应变量转换为右值,或者通过调用std::move()函数来做这件事:

X x1;
X x2=std::move(x1);
X x3=static_cast<X&&>(x2);

想要将参数值不通过拷贝,转化为本地变量或成员变量时,就可以使用这个办法。虽然右值引用参数绑定了右值,不过在函数内部,会当做左值来进行处理:

void do_stuff(X&& x_)
{
  X a(x_);  // 拷贝
  X b(std::move(x_));  // 移动
}
do_stuff(X());  // ok,右值绑定到右值引用上
X x;
do_stuff(x);  // 错误,左值不能绑定到右值引用上

移动语义在线程库中用的比较广泛,无拷贝操作对数据进行转移可以作为一种优化方式,避免对将要被销毁的变量进行额外的拷贝。在2.2节中看到,在线程中使用std::move()转移std::unique_ptr<>得到一个新实例。在2.3节中,了解了std:thread实例可以使用移动语义来转移线程的所有权。

std::threadstd::unique_lock<>std::future<>std::promise<>std::packaged_task<>都不能拷贝,不过这些类都有移动构造函数,能让相关资源在实例中进行传递,并且支持用一个函数将其进行返回。std::stringstd::vector<>也可以拷贝,不过它们也有移动构造函数和移动赋值操作符,就是为了避免拷贝拷贝大量数据。

C++标准库不会将一个对象显式的转移到另一个对象中,除非是将其销毁或赋值的时候(拷贝和移动的操作很相似)。不过,实践中移动能保证类中的所有状态保持不变,表现良好。一个std::thread实例可以作为移动源,转移到新(以默认构造方式)std::thread实例中。std::string可以通过移动原始数据进行构造,并且保留原始数据的状态,不过不能保证的是原始数据中该状态是否正确(根据字符串长度或字符数量决定)。

A.1.2 右值引用和函数模板

使用右值引用作为函数模板的参数时,与之前的用法有些不同:如果函数模板参数以右值引用作为一个模板参数,当对应位置提供左值的时候,模板会自动将其类型认定为左值引用。当提供右值的时候,会当做普通数据使用。可能有些口语化,来看几个例子吧。

考虑一下下面的函数模板:

template<typename T>
void foo(T&& t)
{}

随后传入一个右值,T的类型将被推导为:

foo(42);  // foo<int>(42)
foo(3.14159);  // foo<double><3.14159>
foo(std::string());  // foo<std::string>(std::string())

不过,向foo传入左值的时候,T会被推导为一个左值引用:

int i = 42;
foo(i);  // foo<int&>(i)

因为函数参数声明为T&&,所以就是引用的引用,可以视为是原始的引用类型。那么foo<int&>()就相当于:

foo<int&>(); // void foo<int&>(int& t);

这就允许一个函数模板可以即接受左值,又可以接受右值参数。这种方式已在std::thread的构造函数所使用(2.1节和2.2节),所以能够将可调用对象移动到内部存储,而非当参数是右值的时候再进行拷贝。