C++填坑系列——万能引用+移动语义+完美转发

模板编程中的万能引用、移动语义、完美转发

万能引用:T&&,辅助模板编程,这样左值和右值的参数都可以接收;
移动语义:std::move,转换为右值,也可结合移动构造函数和移动赋值使用;
完美转发:std::forward,可以保留参数的左值和右值属性,因为后续使用该参数可能还需要这个属性;

万能引用

万能引用是一种特殊的引用类型,形式如:T &&param/auto&&

引自isocpp的解释
If a variable or parameter is declared to have type T&& for some deduced type T, that variable or parameter is a universal reference.

也就是说,如果一个变量或者参数被声明为类型T&&,且T是一个被推导的类型,那这个变量或参数就是一个万能引用。

有人说,被称为万能引用universal reference,又被称为转移引用forwarding reference,这两种叫法有什么区别呢?
引自stackoverflowdifference-between-universal-references-and-forwarding-references上的解释,这两种叫法是一个意思

有两种情况:

  1. T&& param

在函数模板中,当一个参数被声明为类型T&&,并且类型T是一个模板类型参数时,这个参数就成为了万能引用;
如果传递给param的是左值,T会被推导为左值引用类型:Sample&,会发生引用折叠;
如果传递给param的是右值,T会被推导为非引用类型:Sample

template <typename T>
T &&my_move1(T &&param) {
  return static_cast<T &&>(param);
}

int main() {
  int a = 10;
  int &b = a;
  int &&c = my_move1(10);  // Ok:传入一个右值,此时T被推导为int,返回值是个右值引用
  // Rvalue reference to type 'int' cannot bind to lvalue of type 'int'
  int &&e = my_move1(a);   // Error:传入一个左值,此时T被推导为int&,发生引用折叠,返回值是个左值引用
  // Rvalue reference to type 'int' cannot bind to lvalue of type 'int'
  int &&d = my_move1(b);   // Error:传入一个左值引用,此时T被推导为int&,发生引用折叠,返回值是个左值引用
}
  1. auto&&

使用方法:auto&& vec = foo();具体的就看下面代码中注释嘞。

class Sample {};
Sample s;
Sample &fun1() { return s; }
Sample fun2() { return s; }

int main() {
  auto &&a = fun1();  // 此时a的类型被推导为Sample&,会发生引用折叠
  auto &&b = fun2();  // 此时b的类型被推导为Sample
}

移动语义

c++11 的新特性,引入是为了方便地将对象所持有的资源从一个实例转移到另一个实例,而非复制资源,可以提升效率,减少资源创建的操作。毕竟复制那种资源占用很大的对象,在申请空间上耗时也会非常高。

std::move的引入,可以使用移动构造函数或移动赋值操作符来转移资源,而不是复制它们。当然具体的移动资源操作如何发生是需要定义在这两个函数中的。

但是注意std::move的操作并非直接进行资源的移动,具体移动的操作是发生在移动构造函数或移动赋值操作符中定义的。std::move只是将传入的对象转换为右值,涉及到右值的构造和赋值会调用上述两个函数。

#include <cstdio>
#include <utility>

class Sample {
 public:
  Sample() : m_p(nullptr), m_size(0) {}

  Sample(int size) : m_p(new int[size]), m_size(size) {
    for (int i = 0; i < size; ++i) {
      m_p[i] = 999;
    }
  }

  Sample(Sample& s) {
    printf("copy constructor called\n");
    m_size = s.m_size;
    m_p = new int[m_size];
    for (int i = 0; i < m_size; ++i) {
      m_p[i] = s.m_p[i];
    }
  }

  Sample& operator=(Sample& s) {
    printf("copy assignment called\n");
    if (this != &s) {
      delete[] m_p;
      m_size = 0;

      m_size = s.m_size;
      m_p = new int[m_size];
      for (int i = 0; i < m_size; ++i) {
        m_p[i] = s.m_p[i];
      }
    }
    return *this;
  }

  /**
   * 移动构造函数:会产生一个新对象:
   * 1. 将新对象的资源权指向已有对象;
   * 2. 移除已有对象的资源权;
   */
  Sample(Sample&& s) {
    printf("move constructor called\n");
    m_p = s.m_p;
    m_size = s.m_size;
    s.m_p = nullptr;
  }

  /**
   * 移动操作符:将一个已有对象移动复制给另一个已有对象,不涉及到产生新对象;
   * 1. 得先判断移动复制的是不是本身自己这个对象,是的话直接返回即可;
   * 2. 先把this对象的资源释放了,然后再给它赋值,他之前的资源肯定也没用了;
   * 3. 然后再把待复制的对象的资源权释放了。
   */
  Sample& operator=(Sample&& s) {
    printf("move assignment called\n");
    if (this != &s) {
      delete[] m_p;
      m_size = 0;

      m_p = s.m_p;
      m_size = s.m_size;
      s.m_p = nullptr;
    }
    return *this;
  }

  ~Sample() {
    if (m_p != nullptr) {
      delete[] m_p;
      m_p = nullptr;
    }
  }

 public:
  int* m_p;
  int m_size;
};

int main() {
  Sample s1;
  Sample s2{s1};
  Sample s3;
  s3 = s1;

  Sample s4{std::move(s1)};
  Sample s5;
  s5 = std::move(s2);
}

std::move的原理就是转换为右值,就是一个static_cast<T&&>的操作;

大概的一个实现代码如下:当然这个实现还是有一些问题,对于注释的第 3 点来说,

T会推导出多种类型,如何满足 T 可能为左值和右值呢?
如上面介绍的万能引用来说,这里T &&param正是如此,问题就在于:
传入左值,T 会被推导为左值引用类型:Sample&,返回的也是一个左值引用;传入右值的话,结果就是正确的。

对于代码Sample s2{my_move1(s1)};的输出,可以看到依旧是调用了拷贝构造函数,这里确实发生了T被推导出来的是个左值;而对于代码Sample s3{my_move1(Sample())};的输出,是没有问题的,推导出的是右值,也调用了移动构造函数。

/**
 * std::move的原理:
 * 1.函数模板,可以操作不同类型
 * 2.std::move==static_cast<T&&>(param)
 * 3.T会推导出多种类型,如何满足T可能为左值和右值呢?+std::remove_reference<T>::type
 * 4.转换为将亡值,真正的“资源移动”发生在移动构造函数function body具体实现中
 */
template <typename T>
T &&my_move1(T &&param) {
  return static_cast<T &&>(param);
}

int main() {
  Sample s1;
  Sample s2{my_move1(s1)};        // copy constructor called
  Sample s3{my_move1(Sample())};  // move constructor called
}

如何解决这个问题呢?

上面的问题是,参数是左值,my_move1返回的是左值;参数是右值,返回的是右值;这并不是我所期望的呀,我希望返回的都是右值!那就需要这里的T推导出来就是本来的类型,不希望加上&的修饰符,不希望返回值会发生引用折叠。

也就是static_cast<T &&>中的T需要被脱掉&这个修饰符才好,正好 c++11 引入的特性std::remove_reference<T>

std::remove_reference<T>的可能实现:基本就是完成对输入参数脱去&这个修饰符的作用

template<class T> struct remove_reference { typedef T type; };
template<class T> struct remove_reference<T&> { typedef T type; };
template<class T> struct remove_reference<T&&> { typedef T type; };

改进后的代码就变成了下这样,同时看到输出也符合我的预期了。

template <typename T>
typename std::remove_reference<T>::type &&my_move2(T &&param) {
  // 先获取去掉引用的T类型(T可能推导出不同的类型)
  using RemoveRefType = typename std::remove_reference<T>::type;
  return static_cast<RemoveRefType &&>(param);
}

int main() {
  Sample s1;
  Sample s2{my_move2(s1)};        // move constructor called
  Sample s3{my_move2(Sample())};  // move constructor called
}

完美转发

std::forward,c++11 的新特性,用于解决在模板函数中参数转发时还能保持参数的左值/右值特性。

Perfect forwarding is there to ensure that the argument provided to a function is forwarded to another function (or used within the function) with the same value category(basically r-value vs. l-value).
引自https://stackoverflow.com/questions/26550603/why-should-i-use-stdforward
也就是说,完美转发被应用于:确保传给一个函数的参数在函数内部使用或者转发给另一个函数时,能够保留一致的类型(右值/左值)

std::move会将任何值转换为右值相比来说;std::forward会将右值转换为右值,左值转换为左值 😃

std::forward的可能实现,传入参数为左值,会调用实现 1,此时param的类型为T&;传入参数为右值,会调用实现 2,此时param的类型为T&&

// 1.
template<typename T>
T&& forward(typename remove_reference<T>::type& param)
{
    return static_cast<T&&>(param); // 会发生引用折叠!
}
// 2.
template<typename T>
T&& forward(typename remove_reference<T>::type&& param)
{
    return static_cast<T&&>(param);
}

接下来测试下,std::forward在代码中的具体表现:

  1. 下面代码中打开TestLvalueOrRvalue(param);,输出结果就是:param lvalue; param lvalue
  2. 下面代码中打开TestLvalueOrRvalue(std::forward<T>(param)),输出结果就是:param lvalue; param rvalue

看起来std::forward之后确实可以符合预期,输入参数为左值s1调用的就是左值参数的函数,输入参数为右值std::move(s1)调用的就是右值参数的函数。哈哈哈,挺好。

class Sample {};

void TestLvalueOrRvalue(Sample &s) { printf("param lvalue\n"); }
void TestLvalueOrRvalue(Sample &&s) { printf("param rvalue\n"); }

template <typename T>
void TestForward(T &&param) {
  // TestLvalueOrRvalue(std::forward<T>(param));
  TestLvalueOrRvalue(param);
}

int main() {
  Sample s1;
  TestForward(s1);  // 参数是个左值
  TestForward(std::move(s1));  // 参数是个右值
}
posted @ 2024-03-02 14:47  战斗天使zzy  阅读(71)  评论(0编辑  收藏  举报