1 左值?右值?

  • 左值(lvaue):赋值符号左边的值。准确来说,左值是表达式(不一定是赋值表达式)后依然存在的持久对象,具有可以访问和识别的内存地址。
  • 右值(rvalue):右边的值,是指表达式结束后就不再存在的临时对象,没有名字,无法修改。

C++11 中为了引入右值引用,将右值的概念进行了进一步的划分,分为:纯右值、将亡值。

  • 纯右值 (prvalue):纯粹的右值
    • 纯粹的字面量,例如 10, true;
    • 表达式求值,结果相当于字面量或匿名临时对象,例如 1+2。
    • 非引用返回的临时变量、运算表达式产生的临时变量、原始字面量、 Lambda 表达式都属于纯右值。
  • 将亡值 (xvalue):C11 为了引入右值引用而提出的概念(因此在传统 C 中,纯右值和右值是同一个概念),也就是即将被销毁、却能够被移动的值。例如函数返回一个vector列表: std::vector<int> v = foo(); 右边即为将亡值。

    2 右值引用

    使用std::move或者&&可以左值变成一个临时的右值,可以被赋值给其他对象。就像Rust中的所有权传递
    标准库中定义如下:从move的定义可以看出,move自身除了做一些参数的推断之外,返回右值引用本质上还是靠static_cast完成的。
    1. template<typename _Tp>
    2. constexpr typename std::remove_reference<_Tp>::type&&
    3. move(_Tp&& __t) noexcept
    4. { return static_cast<typename std::remove_reference<_Tp>::type&&>(__t); }
    5. //所有下面调用是等价的
    6. void func(int&& a)
    7. {
    8. cout << a << endl;
    9. }
    10. int a = 6;
    11. func(std::move(a));
    12. int b = 10;
    13. func(static_cast<int&&>(b));

引入右值后,右值引用可以作为函数的参数,用法如下:

函数定义 允许的参数类型
void function(Type param);
void X::method(Type param);
接受左值或右值作为参数
void function(Type& param);
void function(const Type& param);
void X::method(Type& param);
void X::method(const Type& param);
只接受左值
void function(Type&& param);
void X::method(Type&& param);
只接受右值

函数可能的返回值类型,比传统C++多了右值引用的情况:

函数定义 可能的返回值类型
int function();
int X::method();

- [const] int
- [const] int&
- [const] int&&
int& function();
int& X::method();

- non-const int
- int&
int&& function();
int&& X::method();

- 字面值
- 对象右值引用,int&&

(对象所有权被转移到函数外,生命周期比函数长) |

3 新的构造函数

由于右值引用的引入,对于类定义来说,也相应的多了两种构造函数。类的所有构造函数如下:

  1. class Clazz {
  2. public:
  3. Clazz() noexcept; // 默认构造函数---->Class a;
  4. Clazz(const Clazz& other); //拷贝构造函数---->Class a(b);
  5. Clazz& operator=(const Clazz& other); //复制赋值运算符---->Class a = b;
  6. Clazz(Clazz&& other) noexcept; //新增:Move构造函数---->Class a(std::move(b));
  7. Clazz& operator=(Clazz&& other) noexcept; //新增:Move赋值运算符---->Class a = std::move(b);
  8. virtual ~Clazz() noexcept; //析构函数
  9. };

一个简单的示例如下:

  1. class Data
  2. {
  3. private:
  4. uint16_t age;
  5. uint16_t id;
  6. unsigned char *bytes;
  7. public:
  8. Data(uint16_t _age, uint16_t _id)
  9. : age(_age), id(_id), bytes(nullptr)
  10. {
  11. printf("ctor\n");
  12. }
  13. //移动构造函数
  14. Data(Data &&_data)
  15. {
  16. age = _data.age;
  17. id = _data.id;
  18. bytes = _data.bytes;
  19. //传进来的参数需要清零
  20. _data.bytes = nullptr;
  21. printf("move ctor\n");
  22. }
  23. ~Data()
  24. {
  25. if (bytes)
  26. {
  27. delete[] bytes;
  28. bytes = nullptr;
  29. }
  30. }
  31. };

4 右值引用的用途

4.1 vector push_back

vector.push_back的右值引用,避免拷贝行为:
1530523444714.png

4.2 move构造函数代替拷贝构造函数

用作Class的移动构造函数,避免拷贝构造函数的拷贝行为
1530580211732.png

4.3 move与函数返回值

由于编译器自动优化,我们不需要在函数返回值中显示的调用move。C++11之后,编译器支持返回值优化,具体规则如下:
对于如下函数,编译器会按照下面优先级进行优化

  • 如果X有一个可访问的copy或move构造函数,编译器可以选择省略copy
  • 如果X有一个move构造函数,X被移动。类似显示的调用std::move
  • 如果X有一个复制构造函数,则复制X
  • 两种构造函数都不存在,编译器报错
    1. X foo ()
    2. {
    3. X x;
    4. ...
    5. return x;
    6. }

    5 完美转发std::forward

    完美转发实现了参数在传递过程中保持其值属性的功能,即若是左值,则传递之后仍然是左值,若是右值,则传递之后仍然是右值。
    std::move和std::forward本质都是转换。std::move执行到右值的无条件转换(必然按右值引用转换)。std::forward只有在它的参数绑定到一个右值上的时候,它才转换它的参数到一个右值。std::forward()不仅可以保持左值或者右值不变,同时还可以保持const、Lreference、Rreference、validate等属性不变。

比如下面示例:

  1. class Foo
  2. {
  3. public:
  4. std::string member;
  5. template<typename T>
  6. Foo(T&& member): member{std::forward<T>(member)} {}//构造函数接受一个右值引用
  7. };
  • 传递一个lvalue或者传递一个const lvaue
    • 传递一个lvalue,模板推导之后 T = std::string&,即左值引用
    • 传递一个const lvaue, 模板推导之后T = const std::string&,即左值引用
    • 上面两种情况下,T& &&将折叠为T&,即std::string& && 折叠为 std::string&
    • 最终函数为: Foo(string& member): member{std::forward<string&>(member)} {}
    • std::forward(member)将返回一个左值,最终调用拷贝构造函数
  • 传递一个rvalue
    • 传递一个rvalue,模板推导之后 T = std::string
    • 最终函数为: Foo(string&& member): member{std::forward<string>(member)} {}
    • std::forward(member) 将返回一个右值,最终调用移动构造函数