概述

  1. class A {};
  2. A &a = A(); // 错误
  3. A && a2 = A(); // 正确

从抽象层面理解三种传参方式:

  • 复制传参(T):把你的东西一模一样地给我弄一份,之后我是我,你是你,互不影响
  • 引用传参(T&):我就是你,所有对我的操作都是对你的操作
  • 移动传参(T&&):我把你抢了,你啥都不剩了(实参的所有资源都给形参了,实参只剩一个空壳)

移动传参比复制传参好在哪?复制传参需要把所有底层资源都重新初始化并复制一遍(如动态分配的内存),移动传参不需要,直接抢实参的。

std::move()函数接收一个左值,返回他的右值

  1. int a = 1; // 左值
  2. int &b = a; // 左值引用
  3. // 移动语意: 转换左值为右值引用
  4. int &&c = move(a);
  5. void printInt(int &i) {
  6. cout << "lval ref: " << i << endl;
  7. }
  8. void printInt(int &&i) {
  9. cout << "rval ref: " << i << endl;
  10. }
  11. int main() {
  12. int i = 1;
  13. // 调用 printInt(int&), i是左值
  14. printInt(i);
  15. // 调用 printInt(int&&), 6是右值
  16. printInt(6);
  17. // 调用 printInt(int&&),移动语意
  18. printInt(std::move(i));
  19. }

使用

  1. #include <iostream>
  2. #include <cstring>
  3. #include <vector>
  4. using namespace std;
  5. class MyString {
  6. public:
  7. static size_t CCtor; //统计调用拷贝构造函数的次数
  8. static size_t MCtor; //统计调用移动构造函数的次数
  9. static size_t CAsgn; //统计调用拷贝赋值函数的次数
  10. static size_t MAsgn; //统计调用移动赋值函数的次数
  11. public:
  12. // 构造函数
  13. explicit MyString(const char *cstr = nullptr) {
  14. if (cstr) {
  15. m_data = new char[strlen(cstr) + 1];
  16. strcpy(m_data, cstr);
  17. } else {
  18. m_data = new char[1];
  19. *m_data = '\0';
  20. }
  21. }
  22. // 拷贝构造函数
  23. MyString(const MyString &str) {
  24. CCtor++;
  25. m_data = new char[strlen(str.m_data) + 1];
  26. strcpy(m_data, str.m_data);
  27. }
  28. // 移动构造函数
  29. MyString(MyString &&str) noexcept
  30. : m_data(str.m_data) {
  31. MCtor++;
  32. str.m_data = nullptr; //不再指向之前的资源了,防止资源被释放两次
  33. }
  34. // 拷贝赋值函数 =号重载
  35. MyString &operator=(const MyString &str) {
  36. CAsgn++;
  37. if (this == &str) // 避免自我赋值!!
  38. return *this;
  39. char *tmp = m_data;
  40. m_data = new char[strlen(str.m_data) + 1];
  41. delete[] m_data;
  42. strcpy(m_data, str.m_data);
  43. return *this;
  44. }
  45. // 移动赋值函数 =号重载
  46. MyString &operator=(MyString &&str) noexcept {
  47. MAsgn++;
  48. if (this == &str) // 避免自我赋值!!
  49. return *this;
  50. delete[] m_data;
  51. m_data = str.m_data;
  52. str.m_data = nullptr; //不再指向之前的资源了
  53. return *this;
  54. }
  55. ~MyString() {
  56. delete[] m_data;
  57. }
  58. char *get_c_str() const { return m_data; }
  59. private:
  60. char *m_data;
  61. };
  62. size_t MyString::CCtor = 0;
  63. size_t MyString::MCtor = 0;
  64. size_t MyString::CAsgn = 0;
  65. size_t MyString::MAsgn = 0;
  66. template<typename T>
  67. void move_swap(T &t1, T &t2) // 移动交换
  68. {
  69. T tmp(move(t1));
  70. t1 = move(t2);
  71. t2 = move(tmp);
  72. }
  73. int main() {
  74. vector<MyString> vecStr;
  75. vecStr.reserve(1000); //先分配好1000个空间
  76. for (int i = 0; i < 1000; i++) {
  77. vecStr.push_back(MyString("hello"));
  78. }
  79. cout << "CCtor = " << MyString::CCtor << endl;
  80. cout << "MCtor = " << MyString::MCtor << endl;
  81. cout << "CAsgn = " << MyString::CAsgn << endl;
  82. cout << "MAsgn = " << MyString::MAsgn << endl;
  83. cout << "*******************************" << endl;
  84. MyString s1("world"), s2("hello");
  85. MyString::CCtor = 0;
  86. MyString::MCtor = 0;
  87. MyString::CAsgn = 0;
  88. MyString::MAsgn = 0; // 记录清零
  89. move_swap(s1, s2);
  90. cout << s1.get_c_str() << s2.get_c_str() << endl;
  91. cout << "CCtor = " << MyString::CCtor << endl;
  92. cout << "MCtor = " << MyString::MCtor << endl;
  93. cout << "CAsgn = " << MyString::CAsgn << endl;
  94. cout << "MAsgn = " << MyString::MAsgn << endl;
  95. }
  96. /* 结果
  97. CCtor = 0
  98. MCtor = 1000
  99. CAsgn = 0
  100. MAsgn = 0
  101. *******************************
  102. helloworld
  103. CCtor = 0
  104. MCtor = 1
  105. CAsgn = 0
  106. MAsgn = 2
  107. */

不完美转发

  1. #include <iostream>
  2. #include <cstring>
  3. #include <vector>
  4. using namespace std;
  5. void process(int& i){
  6. cout << "process(int&):" << i << endl;
  7. }
  8. void process(int&& i){
  9. cout << "process(int&&):" << i << endl;
  10. }
  11. void myforward(int&& i){
  12. cout << "myforward(int&&):" << i << endl;
  13. process(i);
  14. }
  15. int main()
  16. {
  17. int a = 0;
  18. process(a); //a被视为左值 process(int&):0
  19. process(1); //1被视为右值 process(int&&):1
  20. process(move(a)); //强制将a由左值改为右值 process(int&&):0
  21. myforward(2); //右值经过forward函数转交给process函数,却成为了一个左值,
  22. // 原因是该右值有了名字 所以是 process(int&):2
  23. myforward(move(a)); // 同上,在转发的时候右值变成了左值 process(int&):0
  24. }

结果
image.png

universal references(通用引用)

  1. template<typename T>
  2. void f( T&& param){
  3. }
  4. f(10); //10是右值
  5. int x = 10; //
  6. f(x); //x是左值

如果上面的函数模板表示的是右值引用的话,肯定是不能传递左值的,但是事实却是可以。这里的&&是一个未定义的引用类型,称为universal references,它必须被初始化,它是左值引用还是右值引用却决于它的初始化,如果它被一个左值初始化,它就是一个左值引用;如果被一个右值初始化,它就是一个右值引用。

注意:只有当发生自动类型推断时(如函数模板的类型自动推导,或auto关键字),&&才是一个universal references

  1. template<typename T>
  2. void f( T&& param); //这里T的类型需要推导,所以&&是一个 universal references
  3. template<typename T>
  4. class Test {
  5. Test(Test&& rhs); //Test是一个特定的类型,不需要类型推导,所以&&表示右值引用
  6. };
  7. void f(Test&& param); //右值引用
  8. //复杂一点
  9. template<typename T>
  10. void f(std::vector<T>&& param); //在调用这个函数之前,这个vector<T>中的推断类型
  11. //已经确定了,所以调用f函数的时候没有类型推断了,所以是 右值引用
  12. template<typename T>
  13. void f(const T&& param); //右值引用
  14. // universal references仅仅发生在 T&& 下面,任何一点附加条件都会使之失效

完美转发

c++中提供了一个std::forward()模板函数解决完美转发问题。

  1. void myforward(int&& i){
  2. cout << "myforward(int&&):" << i << endl;
  3. process(std::forward<int>(i));
  4. }
  5. myforward(2); // process(int&&):2

上述函数的问题是不能转发左值。
解决办法就是借助universal references通用引用类型和std::forward()模板函数共同实现完美转发。

  1. #include <iostream>
  2. #include <cstring>
  3. #include <vector>
  4. using namespace std;
  5. void RunCode(int &&m) {
  6. cout << "rvalue ref" << endl;
  7. }
  8. void RunCode(int &m) {
  9. cout << "lvalue ref" << endl;
  10. }
  11. void RunCode(const int &&m) {
  12. cout << "const rvalue ref" << endl;
  13. }
  14. void RunCode(const int &m) {
  15. cout << "const lvalue ref" << endl;
  16. }
  17. // 这里利用了universal references,如果写T&,就不支持传入右值,而写T&&,既能支持左值,又能支持右值
  18. template<typename T>
  19. void perfectForward(T && t) {
  20. RunCode(forward<T> (t));
  21. }
  22. template<typename T>
  23. void notPerfectForward(T && t) {
  24. RunCode(t);
  25. }
  26. int main()
  27. {
  28. int a = 0;
  29. int b = 0;
  30. const int c = 0;
  31. const int d = 0;
  32. notPerfectForward(a); // lvalue ref
  33. notPerfectForward(move(b)); // lvalue ref
  34. notPerfectForward(c); // const lvalue ref
  35. notPerfectForward(move(d)); // const lvalue ref
  36. cout << endl;
  37. perfectForward(a); // lvalue ref
  38. perfectForward(move(b)); // rvalue ref
  39. perfectForward(c); // const lvalue ref
  40. perfectForward(move(d)); // const rvalue ref
  41. }

深入理解

理解std::move()

右值的目的是指明对象是可移的。
形参都是左值,即使它是一个指向T的右值引用,因为它们可以在栈中取地址。
std::move()只做强制类型转换,返回参数的右值引用,不做任何其他动作。

右值允许被绑定到左值。不要对常量使用std::move()。将值移出对象会改变对象,所以语言不应该允许将常量传递到有可能改变它们的函数(如移动构造函数)。

  1. class Base {};
  2. void f(const Base& b) {
  3. cout << "call f(const Base& b)" << endl;
  4. }
  5. void f(Base&& b) {
  6. cout << "call f(Base&& b)" << endl;
  7. }
  8. int main()
  9. {
  10. const Base b;
  11. f(move(b)); // 传入的类型为const Base&&,不匹配Base&&,但是匹配const Base&
  12. return 0;
  13. }
  14. // call f(const Base& b)

理解std::move()与std::forward()的区别

std::move()无条件地向右值型别转换,而std::forward()仅仅当被传入的实参被绑定到右值时,才对该实参进行向右值的型别转换。在运行期,二者都不会有任何的动作。

区分右值引用与万能引用

万能引用的现身场景:

  1. 模板函数的形参:

template
void f(T&& param);

  1. auto关键字

auto&& var2 = var1;
当&&不涉及型别推导时,那它一定是右值引用。
只有&&前的那个T被推导时才是万能引用。

  1. template<typename T>
  2. void f(vector<T>&& param); // 不是万能引用
  3. int main()
  4. {
  5. vector<Base> v;
  6. f(v); //报错
  7. return 0;
  8. }

const的存在也会让万能引用失效。

  1. template<typename T>
  2. void f(const T&& param); //不是万能引用

将std::move()应用于右值引用,而std::forward()应用于万能引用。

  1. class Widget {
  2. public:
  3. Widget(Widget&& w) :
  4. name(move(w.name)),
  5. p_data(move(w.p_data)) {
  6. // 对右值引用使用move
  7. }
  8. template<typename T>
  9. void setName(T&& param) {
  10. name = forward<T>(param);
  11. //对万能引用使用forward
  12. }
  13. private:
  14. string name;
  15. shared_ptr<Base> p_data;
  16. };

知晓RVO(Return Value Optimization)

对于以值传递返回局部变量的函数,编译器会自动优化避免返回值的复制操作,即使不去避免复制,也会将其强制转换为右值,以进行移动复制操作,因此不用对返回值使用std::move()。

  1. Widget f() {
  2. Widget w;
  3. return w; // 编译器会优化,不会产生复制,即使不优化,也会隐式地转换为下面的形式
  4. }
  5. Widget f() {
  6. Widget w;
  7. return move(w);
  8. }

万能引用的重载问题

对万能引用函数进行重载,几乎总会带来类型匹配问题。因为万能引用模板具化时生成的是精确的类型匹配,当遇到重载函数需要隐式类型转换时(包括添加const),模板具化的函数会优于重载函数。
避免方法可以看《Effective Modern C++》条款27,里面有一个很妙的手法。

引用折叠

在c++中不允许写引用的引用,即auto& &rx = w;非法,但是编译器在编译时会零时产生引用的引用,之后又会产生引用折叠,共四种情况(左-左,左-右-右-左,右-右),只有右-右时会折叠为右值引用,其余情况都是左值引用。

  1. template <typename _Ty>
  2. constexpr _Ty&& forward(remove_reference_t<_Ty>& _Arg) noexcept { // forward an lvalue as either an lvalue or an rvalue
  3. return static_cast<_Ty&&>(_Arg);
  4. }

forward的实现就是利用了引用折叠。
constexpr保证在编译期完成转发,
remove_reference_t<_Ty>& _Arg保证其接受的参数为左值(完美转发时实参为左值)
当_Tp为左值类型(Widget&)时,_Tp&&会被折叠为左值类型,
当_Tp为右值类型(Widget&&)时,_Tp&&会被折叠为右值类型。
除了模板实例化和auto型别推导,typedef和decltype也会产生引用折叠。

  1. template< typename T>
  2. void f(T&&) {
  3. typedef T&& Rvalue_of_T;
  4. }
  5. // 当传入的T为左值型别时(Widget&),Rvalue_of_T是Widget& &&,会被折叠为左值引用Widget&

完美引用失效

  1. 大括号初始化物
  2. 只有声明的static const成员变量
  3. 模板或重载的函数名