A.1 右值引用

    例如:

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

    不能这样写:

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

    1. int const& i = 42;

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

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

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

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

    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. public:
    5. X():
    6. data(new int[1000000])
    7. {}
    8. ~X()
    9. delete [] data;
    10. }
    11. X(const X& other): // 1
    12. data(new int[1000000])
    13. {
    14. std::copy(other.data,other.data+1000000,data);
    15. }
    16. X(X&& other): // 2
    17. {
    18. other.data=nullptr;
    19. }
    20. };

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

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

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

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

    1. void do_stuff(X&& x_)
    2. {
    3. X a(x_); // 拷贝
    4. X b(std::move(x_)); // 移动
    5. do_stuff(X()); // ok,右值绑定到右值引用上
    6. X x;
    7. do_stuff(x); // 错误,左值不能绑定到右值引用上

    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会被推导为一个左值引用:

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