深入认识 右值引用与转发型引用、移动语义及完美转发

ゝ一纸荒年。 2023-03-02 14:58 110阅读 0赞

目录

    • 右值引用
      • 无名右值引用
      • std::move()
      • 具名右值引用
      • 左右值重载策略
    • 移动构造器和移动赋值运算符
      • 移动语义
    • 转发型引用
    • 完美转发
      • std::forward()
    • 加餐:std::move和std::forward源码分析
      • 预备知识
      • std::move
      • std::forward

右值引用

C原始定义

  • 左值:可以出现在赋值号左边或右边
  • 右值:只能出现在赋值号右边

C++左值

  • 传统左值:可以放在等号左边被赋值的值(变量)
  • 新型左值:具名右值引用本身也是左值

右值

  • 不能放在等号左边被赋值操作的值,也就是临时对象,没有变量名,也没有分配内存空间
  • 用于标识临时对象的表达式和与任何对象无关的值(纯右值)、或用于标识即将失效的对象的表达式(失效值)

概述:无论是左值引用,还是右值引用,他们都是引用。而引用就是一种绑定。那么左值引用可以绑定右值吗?右值引用可以绑定左值吗?答案是在某些情况下,左值引用可以绑定右值,右值引用也可以绑定左值。左值引用和右值引用又可以分别细分:

左值引用:

  • 非const左值引用:只能绑定左值
  • const左值引用:即可以绑定左值,也可以绑定右值

右值引用:

  • 无名右值引用:无名右值引用本质上代表的是右值
  • 具名右值引用:具名右值引用本质上是左值,因为有变量名称,还可以用操作符&取地址。但其创建过程只能绑定右值。

    • 具名右值引用也是侠义的右值引用
  • 转发型引用:需要经过推导才能确定类型的T&&右值引用

    • 转发型引用既可以绑定左值,也可以绑定右值。如果绑定左值,转发型引用就是左值引用,如果绑定右值,转发型引用就是右值引用。

无名右值引用具名右值引用的引入主要是为了解决移动语义问题。
转发型引用的引入主要是为了解决完美转发问题。

无名右值引用

无名右值引用(unnamed rvalue reference)是指由右值引用相关操作所产生的引用类型。
无名右值引用主要通过返回右值引用的类型转换操作产生, 其语法形式如下:

  1. static_cast<T&&>(t)

标准规定该语法形式将把表达式 t 转换为T类型的无名右值引用。
无名右值引用右值,标准规定无名右值引用和传统的右值一样具有潜在的可移动性,即它所占有的资源可以被移动(窃取)。

std::move()

由于无名右值引用是右值,借助于类型转换操作产生无名右值引用这一手段,左值表达式就可以被转换成右值表达式。为了便于利用这一重要的转换操作,标准库为我们封装这一操作的函数,这就是std::move()
假设左值表达式t 的类型为T&,利用以下函数调用就可以把左值表达式t转换为T类型的无名右值引用(右值,类型为T&&)。
std::move(t)

具名右值引用

如果某个变量或参数被声明为T&&类型,并且T无需推导即可确定(也就是T的类型是明确的,不是模板参数类型名),那么这个变量或参数就是一个具名右值引用(named rvalue reference)。
具名右值引用是左值,因为具名右值引用有名字,和传统的左值引用一样可以用操作符&取地址。
与广义的右值引用相对应,狭义的右值引用仅限指具名右值引用
传统的左值引用可以绑定左值,在某些情况下也可绑定右值。与此不同的是,具名右值引用只能绑定右值。
具名右值引用和左值引用统称为引用(reference),它们具有引用的共性,比如都必须在初始化时绑定值,都是左值等等。

  1. struct X { };
  2. X a;
  3. X&& b = static_cast<X&&>(a);
  4. X&& c = std::move(a);
  5. //static_cast<X&&>(a) 和 std::move(a) 是无名右值引用,是右值
  6. //b 和 c 是具名右值引用,是左值
  7. X& d = a;
  8. X& e = b;
  9. const X& f = c;
  10. const X& g = X();
  11. X&& h = X();
  12. //左值引用d和e只能绑定左值(包括传统左值:变量a以及新型左值:右值引用b)
  13. //const左值引用f和g可以绑定左值(具名右值引用c),也可以绑定右值(临时对象X())
  14. //具名右值引用b,c和h只能绑定右值(包括新型右值:无名右值引用std::move(a)以及传统右值:临时对象X())

左右值重载策略

有时我们需要在函数中区分参数的左右值属性,根据参数左右值属性的不同做出不同的处理。适当地采用左右值重载策略,借助于左右值引用参数不同的绑定特性,我们可以利用函数重载来做到这一点。常见的左右值重载策略如下:

  1. struct X { };
  2. //左值版本
  3. void f(const X& param1){ /*处理左值参数param1*/}
  4. //右值版本
  5. void f(X&& param2){ /*处理右值参数param2*/}
  6. X a;
  7. f(a); //调用左值版本
  8. f(X()); //调用右值版本
  9. f(std::move(a)); //调用右值版本

即在函数重载中分别重载const左值引用右值引用
重载const左值引用的为左值版本,这是因为const左值引用参数能绑定左值,而右值引用参数不能绑定左值。
重载右值引用的为右值版本,这是因为虽然const左值引用参数和右值引用参数都能绑定右值,但标准规定:右值引用参数的绑定优先度要高于const左值引用参数。

移动构造器和移动赋值运算符

在类的构造器和赋值运算符中运用上述左右值重载策略,就会产生两个新的特殊成员函数:移动构造器(move constructor)和移动赋值运算符(move assignment operator)

  1. struct X
  2. {
  3. X(); //缺省构造器
  4. X(const X& that); //拷贝构造器
  5. X(X&& that); //移动构造器
  6. X& operator=(const X& that); //拷贝赋值运算符
  7. X& operator=(X&& that); //移动赋值运算符
  8. };
  9. X a; //调用缺省构造器
  10. X b = a; //调用拷贝构造器
  11. X c = std::move(b); //调用移动构造器
  12. b = a; //调用拷贝赋值运算符
  13. c = std::move(b); //调用移动赋值运算符

移动语义

无名右值引用具名右值引用的引入主要是为了解决移动语义问题。
移动语义问题是指在某些特定情况下(比如用右值来赋值或构造对象时)如何采用廉价的移动语义替换昂贵的拷贝语义的问题。
移动语义(move semantics)是指某个对象接管另一个对象所拥有的外部资源的所有权。移动语义需要通过移动(窃取)其他对象所拥有的资源来完成。移动语义的具体实现(即一次that对象到this对象的移动(move))通常包含以下若干步骤:

  • 如果this对象自身也拥有资源,释放该资源
  • 将this对象的指针或句柄指向that对象所拥有的资源
  • 将that对象原本指向该资源的指针或句柄设为空值

上述步骤可简单概括为①释放this(this非空时)②移动that
移动语义通常在移动构造器和移动赋值运算符中得以具体实现。两者的区别在于移动构造对象时this对象为空因而①释放this无须进行。

与移动语义相对,传统的拷贝语义(copy semantics)是指某个对象拷贝(复制)另一个对象所拥有的外部资源并获得新生资源的所有权。拷贝语义的具体实现(即一次that对象到this对象的拷贝(copy))通常包含以下若干步骤:

  • 如果this对象自身也拥有资源,释放该资源
  • 拷贝(复制)that对象所拥有的资源
  • 将this对象的指针或句柄指向新生的资源
  • 如果that对象为临时对象(右值),那么拷贝完成之后that对象所拥有的资源将会因that对象被销毁而即刻得以释放

上述步骤可简单概括为①释放this(this非空时)②拷贝that③释放that(that为右值时)
拷贝语义通常在拷贝构造器和拷贝赋值运算符中得以具体实现。两者的区别在于拷贝构造对象时this对象为空因而①释放this无须进行。

比较移动语义与拷贝语义的具体步骤可知,在赋值或构造对象时,

  • 如果源对象that为左值,由于两者效果不同(移动that ≠ 拷贝that),此时移动语义不能用来替换拷贝语义。
  • 如果源对象that为右值,由于两者效果相同(移动that = 拷贝that + 释放that),此时廉价的移动语义(通过指针操作来移动资源)便可以用来替换昂贵的拷贝语义(生成,拷贝然后释放资源)。

由此可知,只要在进行相关操作(比如赋值或构造)时,采取适当的左右值重载策略区分源对象的左右值属性,根据其左右值属性分别采用拷贝语义和移动语义,移动语义问题便可以得到解决。

下面用MemoryBlock这个自我管理内存块的类来具体说明移动语义问题。

  1. // 头文件:typename.hpp
  2. #ifndef TYPENAME_HPP_INCLUDED
  3. #define TYPENAME_HPP_INCLUDED
  4. class MemoryBlock
  5. {
  6. public:
  7. // 构造器(初始化资源)
  8. explicit MemoryBlock(size_t length)
  9. : _length(length)
  10. , _data(new int[length])
  11. {
  12. std::cout << "调用了构造函数\n";
  13. }
  14. // 析构器(释放资源)
  15. ~MemoryBlock()
  16. {
  17. if (_data != nullptr)
  18. {
  19. delete[] _data;
  20. std::cout << "调用了析构函数 destructor.\n";
  21. }
  22. }
  23. // 拷贝构造器(实现拷贝语义:拷贝that)
  24. MemoryBlock(const MemoryBlock& that)
  25. // 拷贝that对象所拥有的资源
  26. : _length(that._length)
  27. , _data(new int[that._length])
  28. {
  29. std::copy(that._data, that._data + _length, _data);
  30. std::cout << "调用了 拷贝构造 函数.\n";
  31. }
  32. // 拷贝赋值运算符(实现拷贝语义:释放this + 拷贝that)
  33. MemoryBlock& operator=(const MemoryBlock& that)
  34. {
  35. if (this != &that)
  36. {
  37. // 释放自身的资源
  38. delete[] _data;
  39. // 拷贝that对象所拥有的资源
  40. _length = that._length;
  41. _data = new int[_length];
  42. std::copy(that._data, that._data + _length, _data);
  43. std::cout << "调用了 拷贝赋值 函数.\n";
  44. }
  45. return *this;
  46. }
  47. // 移动构造器(实现移动语义:移动that)
  48. MemoryBlock(MemoryBlock&& that)
  49. // 将自身的资源指针指向that对象所拥有的资源
  50. : _length(that._length)
  51. , _data(that._data)
  52. {
  53. // 将that对象原本指向该资源的指针设为空值
  54. that._data = nullptr;
  55. that._length = 0;
  56. std::cout << "调用了 移动构造 函数.\n";
  57. }
  58. // 移动赋值运算符(实现移动语义:释放this + 移动that)
  59. MemoryBlock& operator=(MemoryBlock&& that)
  60. {
  61. if (this != &that)
  62. {
  63. // 释放自身的资源
  64. delete[] _data;
  65. // 将自身的资源指针指向that对象所拥有的资源
  66. _data = that._data;
  67. _length = that._length;
  68. // 将that对象原本指向该资源的指针设为空值
  69. that._data = nullptr;
  70. that._length = 0;
  71. }
  72. std::cout << "调用了 移动赋值 函数.\n";
  73. return *this;
  74. }
  75. bool isPtrEmpty()
  76. {
  77. return (!_data)? true : false;
  78. }
  79. private:
  80. size_t _length; // 资源的长度
  81. int* _data; // 指向资源的指针,代表资源本身
  82. };
  83. #endif // TYPENAME_HPP_INCLUDED
  84. // =============================================================================
  85. // 源文件 main.cpp
  86. #include <iostream>
  87. #include "typename.hpp"
  88. using namespace std;
  89. MemoryBlock f() { return MemoryBlock(50); }
  90. int main()
  91. {
  92. MemoryBlock a = f(); // 调用移动构造器,移动语义
  93. cout << "===============\n";
  94. MemoryBlock b = a; // 调用拷贝构造器,拷贝语义
  95. cout << "===============\n";
  96. MemoryBlock c = std::move(a); // 调用移动构造器,移动语义
  97. cout << "===============\n";
  98. cout << "a的指针是否为空:" << a.isPtrEmpty() << endl;
  99. cout << "===============\n";
  100. a = f(); // 调用移动赋值运算符,移动语义
  101. cout << "===============\n";
  102. b = a; // 调用拷贝赋值运算符,拷贝语义
  103. cout << "===============\n";
  104. c = std::move(a); // 调用移动赋值运算符,移动语义
  105. return 0;
  106. }

为了深刻理解函数调用站框架的值传递过程,要避免 编译器 做优化工作,可采用如下方式进行手动编译:

  1. g++ -o test main.cpp -g -fno-elide-constructors
  2. // 编译后运行
  3. .\test // linux改变下斜杠方向 ./test

运行结果如下:
watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzM1ODY2NzM2_size_16_color_FFFFFF_t_70

转发型引用

如果某个变量或参数被声明为T&&类型,并且T需要经过推导才可确定,那么这个变量或参数就是一个转发型引用(forwarding reference)。
转发型引用由以下两种语法形式产生

  • 如果某个变量被声明为auto&&类型,那么这个变量就是一个转发型引用
  • 在函数模板中,如果某个参数被声明为T&&类型,并且T是一个需要经过推导才可确定的模板参数类型,那么这个参数就是一个转发型引用

转发型引用是不稳定的,它的实际类型由它所绑定的值来确定。转发型引用既可以绑定左值,也可以绑定右值。如果绑定左值,转发型引用就成了左值引用。如果绑定右值,转发型引用就成了右值引用。
转发型引用在被C++标准所承认之前曾经被称作万能引用(universal reference)。万能引用这一术语的发明者,Effective C++系列的作者Scott Meyers认为,如此异常灵活的引用类型不属于右值引用,它应该拥有自己的名字。

对于某个转发型引用类型的变量(auto&&类型)来说

  • 如果初始化表达式为左值(类型为U&或U),该变量将成为左值引用(类型为U&)。
  • 如果初始化表达式为右值(类型为U&&或临时值),该变量将成为右值引用(类型为U&&)。

对于函数模板中的某个转发型引用类型的形参(T&&类型)来说

  • 如果对应的实参为左值(类型为U&或U),模板参数T将被推导为引用类型U&,该形参将成为左值引用(类型为U&)。
  • 如果对应的实参为右值(类型为U&&或临时值),模板参数T将被推导为非引用类型U,该形参将成为右值引用(类型为U&&)。

    struct X { };
    X&& var1 = X(); // var1是右值引用,只能绑定右值X();但是var1是左值
    auto&& var2 = var1; // var2是转发型引用,可以绑定左值var1

    1. // var2的实际类型等同于左值var1,即X&,为左值引用;var2是左值

    auto&& var3 = X(); // var3是转发型引用,可以绑定右值X()

    1. // var3的实际类型等同于右值X(),即X&&,右值引用;var3是左值

    template
    void g(std::vector&& param1); // param1是右值引用;因为param1的类型T无需推导
    template
    void f(T&& param2); // param2是转发型引用

    X a;
    f(a); // 模板函数f()的形参param2是转发型引用,可以绑定左值a

    1. // 在此次调用中模板参数T将被推导为引用类型X&
    2. // 而形参param2的实际类型是X&,绑定左值a

    f(X()); // 模板函数f()的形参param2是转发型引用,可以绑定右值X()

    1. // 在此次调用中模板参数T将被推导为非引用类型X
    2. // 而形参param2的实际类型是X&&,绑定右值X()

完美转发

完美转发(perfect forwarding)问题是指函数模板在向其他函数转发(传递)自身参数(形参)时该如何保留该参数(实参)的左右值属性的问题。也就是说函数模板在向其他函数转发(传递)自身形参时,如果相应实参是左值,它就应该被转发为左值;同样如果相应实参是右值,它就应该被转发为右值。这样做是为了保留在其他函数针对转发而来的参数的左右值属性进行不同处理(比如参数为左值时实施拷贝语义;参数为右值时实施移动语义)的可能性。如果将自身参数不分左右值一律转发为左值,其他函数就只能将转发而来的参数视为左值,从而失去针对该参数的左右值属性进行不同处理的可能性。

转发型引用的引入主要是为了解决完美转发问题。在函数模板中需要保留左右值属性的参数,也就是要被完美转发的参数须被声明为转发型引用类型,即参数必须被声明为T&&类型,而T必须被包含在函数模板的模板参数列表之中。按照转发型引用类型形参的特点,该形参T将根据所对应的实参的左右值属性而分别蜕变成左右值引用。但无论该形参成为左值引用还是右值引用,该形参在函数模板内都将成为左值。这是因为该形参有名字,左值引用是左值,具名右值引用也同样是左值。如果在函数模板内照原样转发该形参,其他函数就只能将转发而来的参数视为左值,完美转发任务将会失败。

  1. #include<iostream>
  2. using namespace std;
  3. struct X { };
  4. void inner(const X&) { cout << "inner(const X&)" << endl;}
  5. void inner(X&&) { cout << "inner(X&&)" << endl;}
  6. template<typename T>
  7. void outer(T&& t) { inner(t);}
  8. int main()
  9. {
  10. X a;
  11. outer(a);
  12. outer(X());
  13. }
  14. //inner(const X&)
  15. //inner(const X&)

std::forward()

要在函数模板中完成完美转发转发型引用类型形参的任务,我们必须在相应实参为左值,该形参成为左值引用时把它转发成左值,在相应实参为右值,该形参成为右值引用时把它转发成右值。此时我们需要标准库函数std::forward()
标准库函数 std::forward(t) 有两个参数:模板参数 T 与 函数参数 t。函数功能如下:

  • 当T为左值引用类型U&时,t 将被转换为无名左值引用(左值,类型为U&)。
  • 当T为非引用类型U或右值引用类型U&&时,t 将被转换为无名右值引用(右值,类型为U&&)。

使用此函数,我们在函数模板中转发类型为T&&的转发型引用参数 t 时,只需将参数 t 替换为std::forward<T>(t)即可完成完美转发任务。这是因为

  • 如果 t 对应的实参为左值(类型为U&),模板参数T将被推导为引用类型U&,t 成为具名左值引用(类型为U&),std::forward<T>(t)就会把 t 转换成无名左值引用左值,类型为U&)。
  • 如果 t 对应的实参为右值(类型为U&&),模板参数T将被推导为非引用类型U,t 成为具名右值引用(类型为U&&),std::forward<T>(t)就会把 t 转换成无名右值引用右值,类型为U&&)。

    include

    using namespace std;

    struct X { };
    void inner(const X&) { cout << “inner(const X&)” << endl;}
    void inner(X&&) { cout << “inner(X&&)” << endl;}
    template
    void outer(T&& t) { inner(forward(t));}

    int main()
    {

    1. X a;
    2. outer(a);
    3. outer(X());

    }

    //inner(const X&)
    //inner(X&&)


加餐:std::move和std::forward源码分析

std::movestd::forward是C++0x中新增的标准库函数,分别用于实现移动语义完美转发

预备知识

  1. 引用折叠规则:
    X& + & => X&
    X& + && => X&
    X&& + && => X&&

    注意:引用折叠规则主要用于类型推导

  2. 注:本文中 含义为“即,等价于“。
  3. 函数模板参数推导规则(右值引用参数部分):
    当函数模板的模板参数为T而函数形参为T&&(转发型引用)时适用本规则。
    若实参为左值 U& ,则模板参数 T 应推导为引用类型 U&
    (根据引用折叠规则, U& + && => U&, 而通过形参和实参对照可以发现,T&& ≡ U&,故 T ≡ U& )
    若实参为右值 U&& ,则模板参数 T 应推导为非引用类型 U 。
    (根据引用折叠规则, U或U&& + && => U&&, 而通过形参和实参对照发现T&& ≡ U&&,故T ≡ U或U&&,这里强制规定T ≡ U )
  4. std::remove_reference为C++0x标准库中的元函数,其功能为去除类型中的引用。
    std::remove_reference<U&>::type ≡ U
    std::remove_reference<U&&>::type ≡ U
    std::remove_reference<U>::type ≡ U
  5. 以下语法形式将把表达式 t 转换为T类型的右值(准确的说是无名右值引用,是右值的一种)
    static_cast<T&&>(t)
  6. 无名的右值引用是右值
    具名的右值引用是左值。

std::move

std::move(t) 负责将表达式 t 转换为右值,使用这一转换意味着你不再关心 t 中的内容,它可以通过被移动(窃取)来解决移动语意问题。

源码与测试代码

  1. template<typename _Tp>
  2. // 注意:这一行的typename表示它后面的标识符是类型名,而不是变量名,为了消除歧义
  3. inline typename std::remove_reference<_Tp>::type&&
  4. move(_Tp&& __t)
  5. { return static_cast<typename std::remove_reference<_Tp>::type&&>(__t); }
  6. #include<iostream>
  7. using namespace std;
  8. struct X { };
  9. int main()
  10. {
  11. X a;
  12. X&& b = move(a);
  13. X&& c = move(X());
  14. }

代码说明

  1. 测试代码第9行用X类型的左值 a 来测试move函数,根据标准X类型的右值引用 b 只能绑定X类型的右值,所以 move(a) 的返回值必然是X类型的右值。
  2. 测试代码第10行用X类型的右值 X() 来测试move函数,根据标准X类型的右值引用 c 只能绑定X类型的右值,所以 move(X()) 的返回值必然是X类型的右值。
  3. 首先我们来分析 move(a) 这种用左值参数来调用move函数的情况。
  4. 模拟单步调用来到源码第3行,_Tp&& ≡ X&, __t ≡ a
  5. 根据函数模板参数推导规则,_Tp&& ≡ X& 可推出_Tp ≡ X&
  6. typename std::remove_reference<_Tp>::type ≡ X
    typename std::remove_reference<_Tp>::type&& ≡ X&&
  7. 再次单步调用进入move函数实体所在的源码第4行。
  8. static_cast<typename std::remove_reference<_Tp>::type&&>(__t) ≡ static_cast<X&&>(a)
  9. 根据标准 static_cast<X&&>(a) 将把左值 a 转换为X类型的无名右值引用。
  10. 然后我们再来分析 move(X()) 这种用右值参数来调用move函数的情况。
  11. 模拟单步调用来到源码第3行,_Tp&& ≡ X&&, __t ≡ X()
  12. 根据函数模板参数推导规则,_Tp&& ≡ X&& 可推出 _Tp ≡ X
    13.typename std::remove_reference<_Tp>::type ≡ X
    typename std::remove_reference<_Tp>::type&& ≡ X&&
  13. 再次单步调用进入move函数实体所在的源码第4行
  14. static_cast<typename std::remove_reference<_Tp>::type&&>(__t) ≡ static_cast<X&&>(X())
  15. 根据标准 static_cast<X&&>(X())将把右值 X() 转换为X类型的无名右值引用。
  16. 由9和16可知源码中std::move函数的具体实现符合标准,
    因为无论用左值a还是右值X()做参数来调用std::move函数,
    该实现都将返回无名的右值引用(右值的一种),符合标准中该函数的定义。

std::forward

std::forward(u) 有两个参数:T 与 u。当T为左值引用类型时,u将被转换为T类型的左值引用(属于左值的一种),否则u将被转换为T类型右值(其实是无名右值引用,属于右值的一种)。如此定义std::forward是为了在使用右值引用参数的函数模板中解决参数的完美转发问题。

  1. /// forward (as per N3143)
  2. template<typename _Tp>
  3. inline _Tp&&
  4. forward(typename std::remove_reference<_Tp>::type& __t)
  5. { return static_cast<_Tp&&>(__t); }
  6. template<typename _Tp>
  7. inline _Tp&&
  8. forward(typename std::remove_reference<_Tp>::type&& __t)
  9. {
  10. static_assert(!std::is_lvalue_reference<_Tp>::value, "template argument"
  11. " substituting _Tp is an lvalue reference type");
  12. return static_cast<_Tp&&>(__t);
  13. }
  14. #include<iostream>
  15. using namespace std;
  16. struct X { };
  17. void inner(const X&) { cout << "inner(const X&)" << endl;}
  18. void inner(X&&) { cout << "inner(X&&)" << endl;}
  19. template<typename T>
  20. void outer(T&& t) { inner(forward<T>(t));}
  21. int main()
  22. {
  23. X a;
  24. outer(a);
  25. outer(X());
  26. inner(forward<X>(X()));
  27. }
  28. //inner(const X&)
  29. //inner(X&&)
  30. //inner(X&&)

代码说明

  1. 测试代码第13行用X类型的左值 a 来测试forward函数,程序输出表明 outer(a) 调用的是 inner(const X&) 版本,从而证明函数模板outer调用forward函数在将参数左值 a 转发给了inner函数时,成功地保留了参数 a 的左值属性。
  2. 测试代码第14行用X类型的右值 X() 来测试forward函数,程序输出表明 outer(X()) 调用的是 inner(X&&) 版本,从而证明函数模板outer调用forward函数在将参数右值 X() 转发给了inner函数时,成功地保留了参数 X() 的右值属性。
  3. 首先我们来分析 outer(a) 这种调用forward函数转发左值参数的情况。
  4. 模拟单步调用来到测试代码第8行,T&& ≡ X&, t ≡ a 。
  5. 根据函数模板参数推导规则,T&& ≡ X& 可推出 T ≡ X& 。
  6. forward(t) ≡ forward(t),其中 t 为指向 a 的左值引用。
  7. 再次单步调用进入forward函数实体所在的源码第4行或第9行。
  8. 先尝试匹配源码第4行的forward函数,_Tp ≡ X& 。
  9. typename std::remove_reference<_Tp>::type ≡ X 。
    typename std::remove_reference<_Tp>::type& ≡ X& 。
  10. 形参 __t 与实参 t 类型相同,因此函数匹配成功。
  11. 再尝试匹配源码第9行的forward函数,_Tp ≡ X& 。
  12. typename std::remove_reference<_Tp>::type ≡ X 。
    typename std::remove_reference<_Tp>::type&& ≡ X&& 。
  13. 形参 __t 与实参 t 类型不同,因此函数匹配失败。
  14. 由10与13可知7单步调用实际进入的是源码第4行的forward函数。
  15. static_cast<_Tp&&>(__t) ≡ static_cast(t) ≡ a。
  16. inner(forward(t)) ≡ inner(static_cast(t)) ≡ inner(a) 。
  17. outer(a) ≡ inner(forward(t)) ≡ inner(a)
    再次单步调用将进入测试代码第5行的inner(const X&) 版本,左值参数转发成功。
  18. 然后我们来分析 outer(X()) 这种调用forward函数转发右值参数的情况。
  19. 模拟单步调用来到测试代码第8行,T&& ≡ X&&, t ≡ X() 。
  20. 根据函数模板参数推导规则,T&& ≡ X&& 可推出 T ≡ X 。
  21. forward(t) ≡ forward(t),其中 t 为指向 X() 的右值引用。
  22. 再次单步调用进入forward函数实体所在的源码第4行或第9行。
  23. 先尝试匹配源码第4行的forward函数,_Tp ≡ X 。
  24. typename std::remove_reference<_Tp>::type ≡ X 。
    typename std::remove_reference<_Tp>::type& ≡ X& 。
  25. 形参 __t 与实参 t 类型相同,因此函数匹配成功。
  26. 再尝试匹配源码第9行的forward函数,_Tp ≡ X 。
  27. typename std::remove_reference<_Tp>::type ≡ X 。
    typename std::remove_reference<_Tp>::type&& ≡ X&& 。
  28. 形参 __t 与实参 t 类型不同,因此函数匹配失败。
  29. 由25与28可知22单步调用实际进入的仍然是源码第4行的forward函数。
  30. static_cast<_Tp&&>(__t) ≡ static_cast(t) ≡ X()。
  31. inner(forward(t)) ≡ inner(static_cast(t)) ≡ inner(X())。
  32. outer(X()) ≡ inner(forward(t)) ≡ inner(X())
    再次单步调用将进入测试代码第6行的inner(X&&) 版本,右值参数转发成功。
  33. 由17和32可知源码中std::forward函数的具体实现符合标准,
    因为无论用左值a还是右值X()做参数来调用带有右值引用参数的函数模板outer,
    只要在outer函数内使用std::forward函数转发参数,
    就能保留参数的左右值属性,从而实现了函数模板参数的完美转发。

发表评论

表情:
评论列表 (有 0 条评论,110人围观)

还没有评论,来说两句吧...

相关阅读

    相关 c++move语义引用

    引言        Move语义和右值引用是C++0x加入的最重大语言特性之一。从实践角度讲,它能够完美解决C++中长久以来为人所诟病的临时对象效率问题。从语言本身讲,