首页 > 编程语言 >C++填坑系列——万能引用+移动语义+完美转发

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

时间:2024-03-02 14:57:21浏览次数:30  
标签:std 右值 int 语义 param Sample 填坑 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,右值,int,语义,param,Sample,填坑,C++,&&
From: https://www.cnblogs.com/pplearn/p/18048626

相关文章

  • C++填坑系列——EffectiveModernC++之类型推导
    接下来会记录我在学习《EffectiveModernC++》的一些总结和思考。鉴于C++的知识太多了,我难以全面覆盖到学习,所以这里借此来补充和发散自己的学习心得:)以下内容由学习这个网站EffectiveModernC++的中文翻译内容得来https://cntransgroup.github.io/EffectiveModernCppChine......
  • C++填坑系列——EffectiveModernC++之新手怎么进入现代C++
    新手怎么进入现代C++1.使用auto来自动推导变量类型2.使用{}来创建变量和对象3.使用nullptr来创建空指针4.使用using代替typedef进行别名定义5.使用enumclass代替enum进行枚举定义6.使用=delete来禁止调用一个函数7.使用override来修饰继承链中的重写函数8.使用co......
  • C++填坑系列——EffectiveModernC++之特殊成员函数
    Chapter移步现代c++之特殊成员函数Item17:Understandspecialmemberfunctiongeneration总结:有必要了解各个函数什么时候自动生成;自动生成的函数有可能产生预期外的行为;特殊成员函数(编译器自动生成):默认构造函数,析构函数,拷贝构造函数,拷贝赋值运算符,移动构造函数,移动赋......
  • C++ 多线程笔记2 线程同步
    C++多线程笔记2线程同步并发(Concurrency)和并行(Parallelism)并发是指在单核CPU上,通过时间片轮转的方式,让多个任务看起来像是同时进行的。实际上,CPU在一个时间段内只会处理一个任务,但是由于切换时间非常快,用户感觉像是多个任务同时在进行。这种方式的优点是可以充分利用CPU资源,......
  • C++ 类构造函数 & 析构函数
    带参数的构造函数默认的构造函数没有任何参数,但如果需要,构造函数也可以带有参数。这样在创建对象时就会给对象赋初始值,如下面的例子所示:1#include<iostream>2usingnamespacestd;34classLine5{6public:7voidsetLength(doublelen);8......
  • 万物容器与 c++ 类型反射
    这是一篇组会分享,并且是拖了很长很长时间的那种。这次不会再鸽了这篇文章可以说是针对某cpp佬的公众号的两篇原创内容的笔记c++反射--包容一切的all容器(上)c++反射--包容一切的all容器(中)什么是反射这个好像没有严格的定义,但是概括的说,「反射」是指在程序运行期对程序......
  • C++中cin的详细用法
    1.cin简介cin是C++编程语言中的标准输入流对象,即istream类的对象。cin主要用于从标准输入读取数据,这里的标准输入,指的是终端的键盘。此外,cout是流的对象,即ostream类的对象,cerr是标准错误输出流的对象,也是ostream类的对象。这里的标准输出指的是终端键盘,标准错误输出指的是终端的......
  • C++填坑系列——类型推导 decltype
    decltypedecltype主要是为了解决类型推导的问题,特别是在模板编程和泛型编程中应用较广泛。decltype关键字用于以表达式为参数,推导表达式返回的类型,该类型会保留所有信息。c++11提出的新特性,decltype关键字。和auto一样都是用来做编译时类型推导的,但是也有一些区别:auto:从......
  • C++填坑系列——左值和右值
    c++的表达式首先介绍下c++的表达式是什么?看下cppreference是怎么说的。Anexpressionisasequenceofoperatorsandtheiroperands,thatspecifiesacomputation.也就是说,在C++中,表达式(Expression)是由操作数(Operands)和运算符(Operators)组成的序列。左值和右值就是c++中......
  • C++填坑系列——lambda表达式
    lambda表达式总结:lambda表达式原理:被编译器转换为类+初始化对象的代码;格式:[captureslist](paramslist)specifiersexception->retType{funtionbody}按值捕获和按引用捕获的优缺点以及解决方法;一.lambda原理lambda函数会被编译器转换为类,并定义实现一个operato......