• A.1 右值引用
    • A.1.1 移动语义
    • A.1.2 右值引用和函数模板

    A.1 右值引用

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

    例如:

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

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

    不能这样写:

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

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

    1. int const& i = 42;

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

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

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

    1. int&& i=42;
    2. int j=42;
    3. int&& k=j; // 编译失败

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

    A.1.1 移动语义

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

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

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

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

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

    清单A.1 使用移动构造函数的类

    1. class X
    2. {
    3. private:
    4. int* data;
    5. public:
    6. X():
    7. data(new int[1000000])
    8. {}
    9. ~X()
    10. {
    11. delete [] data;
    12. }
    13. X(const X& other): // 1
    14. data(new int[1000000])
    15. {
    16. std::copy(other.data,other.data+1000000,data);
    17. }
    18. X(X&& other): // 2
    19. data(other.data)
    20. {
    21. other.data=nullptr;
    22. }
    23. };

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

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

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

    1. X x1;
    2. X x2=std::move(x1);
    3. X x3=static_cast<X&&>(x2);

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

    1. void do_stuff(X&& x_)
    2. {
    3. X a(x_); // 拷贝
    4. X b(std::move(x_)); // 移动
    5. }
    6. do_stuff(X()); // ok,右值绑定到右值引用上
    7. X x;
    8. 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 右值引用和函数模板

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

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

    1. template<typename T>
    2. void foo(T&& t)
    3. {}

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

    1. foo(42); // foo<int>(42)
    2. foo(3.14159); // foo<double><3.14159>
    3. foo(std::string()); // foo<std::string>(std::string())

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

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

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

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

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