一、右值

左值、右值。顾名思义,就是可以出现在=(赋值运算符)左边和右边的值。看下面几个例子:

  1. int func(){ return 2; }
  2. int main(){
  3. func() = 2;
  4. return 0
  5. }
  6. // 编译错误:
  7. // error: lvalue required as left operand of assignment
  8. // =操作符的左边必须是左值。
  9. // **********************************************************************
  10. int& func(){
  11. return 2;
  12. }
  13. int main(){
  14. return 0;
  15. }
  16. // 编译错误
  17. // error: invalid initialization of non-const reference of type 'int&' from an rvalue of type 'int'
  18. // 右值赋值给非const引用是不正确的初始化。
  • 左值
    • 最先在C中定义,“可以出现在=操作符左边的值”。
    • 随着C特性的增加,有些特殊的左值不能出现在=操作符的左边。
      • 数组类型
      • 不完整类型
      • const类型
        • 若是struct、union,则只要它的任意成员(递归地)含有const。
    • 左右值很难有一个精确定义,可以按以下理解:
      • 左值是一个表示内存位置的表达式,可以通过&取地址符获得内存地址
  • 右值
    • 表达式不是左值,就是右值。也就是说上面的这些特殊的左值也是右值。
    • 左值可以代替右值,反过来不行,这很好理解,可以直接去左值内存里的值(就是右值了)。
  1. int var; // 左值
  2. var = 4; // 4,右值。
  3. 4 = var; // 错误,4是右值
  4. (var + 1) = 2; // 错误,var+1的值是右值。
  5. const int var = 4; // 右值,不可修改的左值是右值。
  6. int a = 42;
  7. int b = 43;
  8. // a、b都是左值
  9. a = b; // ok
  10. b = a; // ok
  11. a = a * b; // ok
  12. // a * b是右值
  13. int c = a * b; // ok, rvalue on right hand side of assignment
  14. a * b = 42; // error, rvalue on left hand side of assignment
  15. // 左值:
  16. //
  17. int i = 42;
  18. i = 43; // ok, i is an lvalue
  19. int* p = &i; // ok, i is an lvalue
  20. int &foo();
  21. foo() = 42; // ok, foo() is an lvalue
  22. int* p1 = &foo(); // ok, foo() is an lvalue
  23. // 右值:
  24. //
  25. int foobar();
  26. int j = 0;
  27. j = foobar(); // ok, foobar() is an rvalue
  28. int* p2 = &foobar(); // error, cannot take the address of an rvalue
  29. j = 42; // ok, 42 is an rvalue
  1. // *****************************************************************
  2. // 右值可以用左值代替
  3. int a = 1; // a 是左值
  4. int b = 2; // b 是左值
  5. int c = a + b; // + 需要右值,所以 a 和 b 被转换成右值
  6. // + 返回右值
  7. string& str = std::string(); // 错误,str是左值引用,string()是右值
  8. // *****************************************************************
  9. // 左值不能用右值代替
  10. int main(){
  11. fuck1("asdfasdf"); // 编译错误,右值不能赋值给左值引用
  12. fuck2("asdfasdf"); // 编译通过,右值可以赋值给常量的引用。
  13. }
  14. // 常量引用可以用右值赋值,可以避免创建临时对象。
  15. void fuck1(string& a) {
  16. return;
  17. }
  18. void fuck2(const string& a) { // 因此C++中经常使用const类型引用。
  19. return;
  20. }

二、右值引用

  1. A a;
  2. A& ref1 = a; // 左值引用,本来就叫引用,但是为了和右值引用区别,就叫左值引用 。
  3. A&& ref2 = a; // 右值引用

右值引用是C++11的特性。右值引用的设计目的是为了解决以下两个问题:

  • 实现移动语义(Move Semantics)
    • 为程序员提供一种控制函数重载的机制,根据实参是左值/右值类型,来调用对应的重载函数。
    • 具体场景:为类提供移动构造/赋值函数,让类既支持对象拷贝又支持对象移动,如std::vector的对象移动可以避免创建临时对象带来的高额开销。
  • 完美转发(Perfect Forwarding)
    • 为程序提供一种完美的参数传递机制,若实参是左值类型,则以左值引用类型传递;若是实参是右值类型,则以右值引用类型传递。这样既可以传递左值(避免值传递),又可以传递右值,触发转移语义。
    • 具体场景:见下面的factory函数。

      1、移动语义(move semantics)

      假设以下场景:
  1. template<typename T>
  2. class Base {
  3. public:
  4. Base() = default;
  5. ~Base() = default;
  6. Base& operator=(const X&); // 拷贝赋值
  7. private:
  8. T* m_pResources; // 假设m_pResources是一个拷贝代价很大的数据
  9. // 比如是一个很大的数组。
  10. }
  11. template<typename T>
  12. Base& Base<T>::operator=(const Base& rhs) {
  13. // 以下是拷贝赋值的伪代码:
  14. if(&rhs == this) return *this; // 查重
  15. T* tmp = copy(rhs.m_pResource); // 拷贝一份rhs的副本
  16. clear(m_pResource); // 清除自身的数据
  17. m_pResource = tmp; // 绑定到拷贝的副本。
  18. return *this;
  19. }
  20. // *************************************************************************
  21. Base<int> v1;
  22. ......; // 在v1中填充了数据
  23. Base<int> v2;
  24. v2 = v1; // 这里原本的目的是将v1的数据转移到v2中。
  25. // 而实际上触发的是拷贝构造,因此会带来巨大的拷贝开销,大大降低性能。

假如我们是C++的设计者,我们该如何改进?我们可以这样做:再增加两个类似拷贝构造和拷贝赋值的函数,这两个函数负责完成将参数的数据转移到本对象中。如下:

  1. template<typename T>
  2. Base<T>::Base(UnknownType rhs) { // 构造函数,将rhs的数据转到this中,避免临时拷贝创建
  3. ......
  4. }
  5. // 上面这个函数其实就是移动构造函数,UnknownType其实就是右值引用(X&&)。
  6. template<typename T>
  7. Base& Base<T>::operator=(UnknownType rhs) {
  8. // 以下是将rhs的数据移动到this上的伪代码:
  9. if(&rhs == this) return *this; // 查重
  10. swap(m_pResource, rhs.m_pResource); // 交换数据,就达到了转移数据的目的。
  11. return *this;
  12. }
  13. // 上面这个函数其实就是移动赋值函数,

何时触发拷贝赋值还是新设计的函数(移动赋值函数)?那就要看赋值运算符(=)的右边是左值还是右值,当是左值时,优先匹配拷贝赋值;当是右值时,优先匹配移动赋值。
还有一个问题,虽然提供了移动接口,但是程序员好像并不能决定使用哪个,因为程序员不能决定一个值是左值还是右值。因此还需要提供一个函数move,它一定会返回一个右值:

  1. int leftVal = 1; // leftVal是一个左值
  2. int &&rRef = std::move(leftVal); // 正确,rRef是leftVal的右值引用。
  3. // move返回leftVal的右值。
  4. int && rRef = leftVal; // 错误,右值引用无法绑定到左值。

至此,程序员终于可以自己决定出发拷贝赋值还是移动赋值。

  1. Base<int> v1;
  2. ......; // 在v1中填充了数据
  3. Base<int> v2;
  4. v2 = v1; // 触发拷贝赋值,因为=赋值运算符的右边是左值
  5. v2 = std::move(v1); // 触发移动赋值,因为=赋值运算符的右边是右值引用。

注意移动赋值里的swap函数的实现:

  1. // 普通版本的swap,并没有出现右值,因此也就不会触发T的移动构造、移动赋值。
  2. template<typename T>
  3. void swap(T& a, T& b) {
  4. T tmp(a);
  5. a = b;
  6. b = tmp;
  7. }
  8. // move版本的swap
  9. using std::move;
  10. template<typename T>
  11. void swap(T& a, T& b){
  12. T tmp(move(a)); // move(a)返回右值,触发T的移动构造函数。
  13. a = move(b); // move(b)发回右值,触发移动赋值函数。
  14. b = move(tmp); // 触发移动赋值函数。
  15. }

右值引用是不是右值?
答:有名字的右值引用是左值,没有名字的右值引用,才是右值,我们可以记忆为if-it-has-a-name rule。见如下代码:

  1. void foo(Base&& x){
  2. Base x1 = x; // 触发拷贝构造,x并不是右值。右值引用x的名字是x。
  3. ......
  4. }
  5. Base& goo();
  6. Base x = goo(); // 触发移动构造,goo()返回值是右值,没有名字。
  7. // 为什么要这样?其实这样才合理。因为foo中的x理应在整个函数体内都可以被使用。
  8. // 若x此时是右值,而x1=x移动构造,这是一种有潜在破坏性的拷贝,x的数据很有可能已经无效。
  9. // 这显然是不合理的,因此才有if-it-has-a-name rule。

当Derived继承Base时,且Derived也有移动构造/赋值,请注意:

  1. class Dervied : public Base {
  2. public:
  3. ......
  4. Derived(Derived&& d);
  5. Derived& operator=(Derived&& d) noexcept; // 移动构造一般不会报异常。
  6. }
  7. Dervied::Dervied(Dervied && d)
  8. :Base(std::move(d)) // 正确,调用Base的移动构造
  9. //:Base(d) // 错误,d是左值,调用Base的拷贝构造
  10. {
  11. }
  12. Derived& Dervied::operator=(Derived&& d) noexcept{
  13. Base::operator=(d); // 千万别忘了,调用Base的移动赋值。
  14. ......
  15. }

注意!谨慎使用std::move,否则可能事与愿违(性能下降):

  1. Base foo(){
  2. Base ret;
  3. ......
  4. return ret; // 触发拷贝构造。
  5. return std::move(ret); // 性能可能还会下降。
  6. }
  7. // ************************************************
  8. int main(){
  9. auto fuck = foo();
  10. // 在现代编译器中,这种代码会被优化,foo并不是返回一个ret的拷贝,
  11. // 而是直接在调用处直接构造ret,因此整个过程只触发了一次拷贝构造,
  12. // 如果使用return std::move(),反而会阻止编译器的优化。
  13. }

2、完美转发(Perfect Forwarding)

考虑以下场景:

  1. template<typename T, typename Arg>
  2. T* factory(Arg arg){
  3. return new T(arg);
  4. }
  5. // factory是一个常见的参数转发函数,将ar参数转发给T的构造函数。
  6. // 显然这是很失败的factory,因为arg是值传递(多余拷贝),很快我们想到改成引用版本:
  7. //**********************************************************************************
  8. ......
  9. T* factory(Arg& arg){
  10. ......
  11. }
  12. // 这避免了值传递,但是arg的实参只能是左值
  13. factory(foo()); // 错误,foo的返回值是右值
  14. factory(4); // 错误,4是右值。
  15. // 在左右值中知识中,我们知道,const Arg& 是可以赋值右值的,因此我们可以再加一个常量引用版本
  16. //**********************************************************************************
  17. ......
  18. T* factory(const Arg& arg){
  19. ......
  20. }
  21. // 到目前为止,左右值都可以转发,但是不够完美:
  22. // 1、一个参数就有两个版本的factory,代码太多了。
  23. // 2、arg始终都是左值,在内部并不能触发移动语义。
  24. // 显然还是要靠右值引用。

C++的函数参数类型推导规则,包含两部分:

  • 第一,引用折叠规则
    • A& & 变成 A&
    • A& && 变成 A&
    • A&& & 变成 A&
    • A&& && 变成 A&&
  • 第二,模板参数推导规则(有模板的时候)
    • 若实参是A类型的左值,则T推导为A&
    • 若实参是A类型的右值,则T推导为A&&,见下面代码:
  1. template <typename T>
  2. void test( T &&val ){}
  3. void main()
  4. {
  5. int i = 0;
  6. const int ci = i;
  7. // 实参是int的左值,T推断为int&,val的类型是int& &&,折叠为int&
  8. test( i );
  9. // 实参是int的特殊左值,T推断为const int&,val的类型是const int& &&,折叠为const int&
  10. test( ci );
  11. // 实参是int的右值,T推断为T,val的类型是T&&
  12. test( i * ci );
  13. return;
  14. }

根据以上规则,我们可以设计一个函数,使得传入的是左值类型参数时,以左值引用类型参数传递,当传入的是右值类型参数时,以右值引用类型参数传递,这样的话就可以完美转发左右值参数了,如下设计:

  1. template<typename T>
  2. T* factory(Arg&& arg){
  3. return new T(std::forward<Arg>(arg));
  4. }
  5. template<class Arg>
  6. Arg&& forward(typename remove_refernece<Arg>::type& a) noexcept{
  7. return static_cast<Arg&&>(a);
  8. }
  9. // ********************************************************************************
  10. // 假设实参是左值类型参数,代码:
  11. X x;
  12. factory<A>(x);
  13. // 根据上面的模板参数推导规则,x是X类型的左值,则Arg推断为X&,模板代码变为:
  14. template<typename T>
  15. T* factory(X& && arg){
  16. return new T(std::forward<X&>(arg))
  17. }
  18. template<class S>
  19. X& && forward(typename remove_refernece<X&>::type& a) noexcept{
  20. return static_cast<X& &&>(a);
  21. }
  22. // ***************************************
  23. // 根据引用折叠规则,模板代码变为:
  24. template<typename T>
  25. T* factory(X& arg){
  26. return new T(std::forward<X&>(arg));
  27. }
  28. template<class S>
  29. X& forward(X& a) noexcept{
  30. return static_cast<X&>(a);
  31. }
  32. // 参数arg是左值类型,且在factory、forward函数中是以左值引用方式传递,避免了值拷贝
  33. // 这是完美的左值类型参数传递(转发)方案。
  34. // ********************************************************************************
  35. // 假设实参是右值类型参数,代码:
  36. X foo();
  37. factory<A>(foo());
  38. // 实参类型是X&&,则Arg推断为X,模板代码变为:
  39. template<typename T>
  40. T* factory(X&& arg){
  41. return new T(std::forward<X>(arg));
  42. }
  43. template<class S>
  44. X&& forward(X& a) noexcept{
  45. return static_cast<X&&>(a);
  46. }
  47. // 参数类型是右值类型,且在factory、forward中是以右值引用方式传递,会触发转移语义
  48. // 这正是完美的右值类型参数传递方案。

综上,这个方案确实就是完美转发方案。

回头再看下std::move的可能实现:

  1. template<class T>
  2. typename remove_reference<T>::type&& std::move(T&& a) noexcept
  3. {
  4. typedef typename remove_reference<T>::type&& RvalRef;
  5. return static_cast<RvalRef>(a);
  6. }
  7. // 通过假设两种场景来验证一下
  8. // 场景一:传入左值类型参数
  9. // 场景二:传入右值类型参数
  10. // *********************************************************************************************
  11. // 场景一:假设如下代码:
  12. X x;
  13. std::move(x); // 此时x是左值,看move如何返回右值引用。
  14. // 根据上面的参数类型推导规则,实参是左值类型,T推断为X&,代码变为:
  15. template<class T>
  16. typename remove_reference<T&>::type&& std::move(T& && a) noexcept
  17. {
  18. typedef typename remove_reference<T&>::type&& RvalRef;
  19. return static_cast<RvalRef>(a);
  20. }
  21. // 进行引用折叠,去掉remove_reference,得到代码:
  22. template<class T>
  23. T&& std::move(T& a) noexcept
  24. {
  25. return static_cast<T&&>(a);
  26. }
  27. // ****************************************
  28. // 场景二:假设代码如下:
  29. X foo();
  30. std::move(foo());
  31. // 同上理推导出代码为:
  32. template<class T>
  33. T&& std::move(T&& a) noexcept
  34. {
  35. return static_cast<T&&>(a);
  36. }
  37. // 这乍一看,直接调用static_cast不就行了嘛,事实是确实可以,为了规范,还是要调用std::move

三、移动语义的except

这里十分建议你遵守以下两个规则:

  • 尽力将移动构造函数/移动赋值函数设计成不会抛出异常
  • 并将函数声明成noexcept。 ```cpp

template class vector { public: …… vector( vector&& ) noexcept; vector& operator=( vector&& ) noexcept; …… }

  1. 这样建议的主要原因是,假设是std::vector,如果没有遵守上面两条规则,则std::vector gets resized的时候,不会触发转移语义,即已有元素不会relocated到新的内存块。Scott Meyers的《Effective Modern C++》中有详细说明。
  2. <a name="nZQOm"></a>
  3. # 四、总结
  4. ```cpp
  5. void foo(T& t){} // t是左值时,匹配此函数
  6. void foo(T&& t){ // 当t是右值时,匹配此函数
  7. // 当进入到函数内,t就不再是右值,而是左值,因为t有名字(t)。
  8. T f = std::move(t); // 需要再触发移动语义,必须显式再调用std::move
  9. }
  10. T t;
  11. foo(t); // 匹配foo(T&)
  12. foot(std::move(t)) // 匹配foo(T&&)
  13. // std::move返回对象的右值引用。
  14. std::forward<T&>(t); // 返回值是左值引用
  15. std::forward<T&&>(t); // 返回值是右值引用。
  16. std::forward<T>(t); // 返回值是右值引用。
  17. // std::move,让程序员可以获得指定对象的右值引用
  18. // std::forward,让程序员可以获得指定对象的左值引用或右值引用。