avatar

Catalog
C++11 中的右值引用和移动语义

右值引用

  • C++11 引入,所谓右值引用,赋予了程序员修改右值的能力。在引入右值引用之前,是没有办法修改这个右值的,也即这个右值所在地址中的值不能被修改,只能一直是这个右值(可能存在一个误区:右值不能取地址,但不代表右值没有地址):

    c++
    1
    2
    int &x = 5;       // Error
    const int &x = 5; // OK

    可见虽然可以将右值赋值给左值引用,但这个左值引用必须是常量引用,仍然无法修改其值。

    引入右值引用后,就可以将右值赋给右值引用,然后修改:

    c++
    1
    2
    int &&x = 5;
    x = 6;

移动语义

  • 有了右值引用以后,就可以用来实现移动语义。移动是相对拷贝而言的,例如拷贝构造和拷贝赋值。如果被拷贝的对象是一个右值,实际上就没有必要真的拷贝了,将其内容直接转移给新对象,然后自己变成一个 null (即移动语义)可以节省一次拷贝的开销。

  • 利用移动语义的具体方法是实现移动构造和移动赋值:

    c++
    1
    2
    3
    4
    A(const A& a);             // 拷贝构造
    A& operator=(const A& a); // 拷贝赋值
    A(A&& a); // 移动构造
    A& operator=(A&& a); // 移动赋值

    对于实现了移动构造的类,编译器会根据用来构造的对象是左值还是右值自动调用拷贝构造或者移动构造。但是如果没有实现移动构造,那么不管用来构造的对象是左值还是右值,都只会调用拷贝构造。(前提是实现了拷贝构造)。总结一下:

    用左值构造 用右值构造
    未实现拷贝构造和移动构造 默认拷贝构造 默认移动构造
    实现了拷贝构造,未实现移动构造 拷贝构造 拷贝构造
    未实现拷贝构造,实现了移动构造 非法行为 移动构造
    实现了拷贝构造和移动构造 拷贝构造 移动构造

    所谓默认构造是说如果一个类没有显示声明拷贝构造或者移动构造,而又用到这些函数时,编译器会生成一个默认版本,默认版本的实现是:

    • 对于类中的对象成员,调用它们的拷贝构造或者移动构造(取决于该默认版本是默认拷贝还是默认移动)
    • 对于类中的基础类型数据成员(数组除外,例如指针,int),直接赋值(浅复制)
    • 对于类中的数组成员,将数组元素一个个复制过去(深复制)
  • 利用 std::move() 函数可以将一个左值转换成一个右值,这样可以使用移动语义,当构造之后不再需要这个左值时,该函数特别有用。

完美转发

  • 传参的时候,左值引用只能接受左值,右值引用只能接受右值,所以有时候需要进行重载:

    c++
    1
    2
    void f(int &x) {}
    void f(int &&x) {}
  • 完美转发利用了 c++ 标准中的两个机制:reference collapsing 和 special type deduction:

    • reference collapsing 是说对于 T& 或是 T&& 这样的类型,T 有可能也是个引用,所以需要有一个规则将引用的引用 collapse 成引用:

      • T& & -> T&
      • T& && -> T&
      • T&& & -> T&
      • T&& && -> T&&

      简言之,只要有 &,collapse 的结果就是 &

    • special type deduction 是说对于 T&& 这样的类型,会进行一次类型推断,如果是左值(例如 int 左值)则将 T 推断为 int&,所以 T&& 就是 int& &&int &;如果是右值(例如 int 右值)则将 T 推断为 int,所以 T&& 就是 int&&

      所以 T&& 又叫 forwarding reference(也叫 universal reference)

      需要注意的是 int, int&, int&& 都是左值(右值引用 ≠ 右值)

    reference

  • 完美转发利用上述两个机制在不用重载的情况下区分出入参的左右值属性:

    • 实参是左值时,推断为左值引用
    • 实参是右值时,推断为普通类型
    c++
    1
    2
    3
    4
    5
    6
    7
    8
    template<typename T>
    void f(T &&x) {
    h(forward<T>(x));
    }

    int a = 1;
    f(a); // void f<int &>(int &x)
    f(1); // void f<int>(int &&x)

    再配合上 std::forward() 函数,保留 x 的左 / 右值属性,做到完美转发。(forward 保留左 / 右值属性,而 move 强转成右值),事实上保留左右值属性的说法并不准确(因为右值引用仍然是左值),应该是将左值引用变为左值,将右值引用变为右值。

    如果没有 forward,直接写 h(x),那么 x 会被认为是左值,即使有可能是右值引用(事实上右值引用也是左值),所以 forward 的作用在于将右值引用变成右值(左值被作用于一个表达式变成了右值)

Reference

Author: Gusabary
Link: http://gusabary.cn/2020/03/18/C++11%E4%B8%AD%E7%9A%84%E5%8F%B3%E5%80%BC%E5%BC%95%E7%94%A8%E5%92%8C%E7%A7%BB%E5%8A%A8%E8%AF%AD%E4%B9%89/
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.

Comment