01-----C++11的std::move和std::forward完美转发

一 std::move

1. std::move的功能和优点
功能:返回传入参数的右值引用。右值引用的概念是在C++11才提出来的。在此之前只有一种引用。说白点,就是将所有权进行转移,若转移成功后,所有权到达新的对象,原对象不再拥有所有权,再次访问会发生未知错误,例如段错误,未知值等等。或者说,move 只是纯粹的将一个左值转化为了一个右值。

优点:调用此函数不会引起任何数据争用。(Calling this function introduces no data races.)
需要注意的重点是:

  • 1)如果只是调用std::move然后使用右值引用接收后,原对象仍然拥有该内存的所有权,因为只是获取了右值引用,并未完全移动给另一个对象。
  • 2)如果该右值是以匿名对象的方式传入,则原对象的所有权丢失。
  • 3)std::move主要是针对对象来处理的,对于应用在指针的意义不大(我简单测试了一下,不会报错,可以move指针),因为std::move的出现就是为了让大家少用指针,多用引用,并且因为在引用后,函数内部的对象传递仍可能可能存在拷贝,所以增加std::move和std::forward完美转发。

例如:

//1)
string A("abc");
string &&Rval = std::move(A);           // move完之后是一个右值,所以必须是&&才是取右值引用;少一个&代表左值引用,但会报错,因为右值无法给左值做引用。注意这里只是获取了A的引用,并未实现move转移。
string B(Rval);                         // this is a copy , not move.并且注意,这里只是简单的传右值引用,那么它也只是拷贝,而不会真正移走所有权,即上面的注意点 1,即使运行了这一行代码,A仍然拥有所有权,B只是copy了一份。

//2)
string A("abc");
string C(std::move(A));                 // 和上面不一样的是,虽然也是返回一个右值引用,但实际可以认为是匿名对象,A的所有权照样会被夺去,因为我们学过,当一个匿名对象被刚创建的对象接收时,他是不会重新创建内存的,所以此时C已经拥有所有权,A丢失
cout << A << endl;                      // output ""

cout << C << endl;                      // output "abc",因为A的所有权通过匿名对象到了C当中。

关于上面的代码,我们需要在下面的完美转发std::forward才能进一步对比分析。

下面先看std::move的移动所有权。

#include <iostream>
#include <vector>
#include <string>
using namespace std;

void test01()
{
  std::string vs   = "vs-string";
  std::string code = "code-string";
  std::vector<std::string> v;
 
  v.push_back(vs);                      // copy
  v.push_back(std::move(code));         // move,匿名对象传参。
 
  std::cout << "v contains:";
  for (std::string& x:v) 
    std::cout << ' ' << x;
  std::cout << std::endl;
  
  std::cout << "vs:" << vs << std::endl;        // output: "vs:vs-string"
  std::cout << "code:" << code << std::endl;    // output: ""
}

void test02()
{
  std::string vs   = "vs-string";
  std::string code = "code-string";
  std::vector<std::string> v;
 
  v.push_back (vs);								// copy
  v.push_back (code);							// copy
 
  std::cout << "v contains:";
  for (std::string& x : v) 
    std::cout << ' ' << x;
  std::cout << std::endl;
  
  std::cout << "vs:" << vs << std::endl;        // output: "vs:vs-string"
  std::cout << "code:" << code << std::endl;    // output: "code:code-string"
}

int main()
{
    test01();
    test02();

    return 0;
}

test01输出结果,因为code被成功的移动所有权,所以此时code默认是被赋一个空值。
在这里插入图片描述

test02输出结果,由于只是正常的拷贝,所以不会进行所有权的更改,code正常输出。
在这里插入图片描述

二 std::forward完美转发

当我们将一个右值引用传入函数时,他在实参中有了命名,所以继续往下传或者调用其他函数时,根据C++ 标准的定义,这个参数变成了一个左值。那么他永远不会调用接下来函数的右值版本,这可能在一些情况下造成拷贝。
为了解决这个问题 C++ 11引入了完美转发,根据右值判断的推倒,调用forward 传出的值,若原来是一个右值,那么他转出来就是一个右值,否则为一个左值。
这样的处理就完美的转发了原有参数的左右值属性,不会造成一些不必要的拷贝。

#include <iostream>
#include <vector>
#include <string>
using namespace std;

void test03()
{
    string A("abc");
    string &&Rval = std::move(A);           // move完之后是一个右值,所以必须是&&才是取右值引用;少一个&代表左值引用,但会报错,因为右值无法给左值做引用。注意这里只是获取了A的引用,并未实现move转移。
    string B(Rval);                         // this is a copy , not move.  Rval不是一个匿名对象,是一个简单的右值引用,所以不会被夺走所有权。
    cout << A << endl;                      // output "abc"

    string C(std::forward<string>(Rval));   // move.
    cout << A << endl;                      // output ""
    cout << C << endl;                      // output "abc",因为A的值通过Rval完美转发到了C当中。
}

//这个test04是为了验证通过获取std::move的右值引用后再传参数,与直接传std::move作为参数返回匿名对象的区别。
void test04()
{
    string A("abc");
    string C(std::move(A));                 // 和上面不一样的是,虽然也是返回一个右值引用,但实际可以认为是匿名对象,A的所有权照样会被夺去,因为我们学过,当一个匿名对象被刚创建的对象接收时,他是不会重新创建内存的,注意对比test03
    cout << A << endl;                      // output ""

    cout << C << endl;                      // output "abc",因为A的所有权通过匿名对象到了C当中。
}

int main()
{
    test01();
    test02();
    test03();
	test04();
	
    return 0;
}

test03结果:
在这里插入图片描述

分析test03,一开始我们分析了move的作用,首先它只是先获取A的右值引用,并未进行了所有权的移动。
然后执行string B(Rval);时,由于只是简单的传右值引用变量,所以此时B只是拷贝了一份,A此时仍然拥有该内存所有权。注意对比std::move的test01中的v.push_back(std::move(code)),vector的push_back是支持右值的并且认为其是一个匿名对象,所以此时code的所有权就已经被成功移动了,code值必定为空,而这里的B只是拷贝,不会移动所有权。
所以第一行输出"abc"就是这个意思。

test04结果:
在这里插入图片描述
第一行输出了空字符串,是由于直接传std::move作为参数,string的构造认为是一个匿名对象,那么直接夺走A的所有权给C,所以C输出了abc。

三 那么如何解决std::move传右值后(即上面std::move的注意点1),在被调用的函数内部仍被转成左值的方法呢?

答:就是test03里面的std::forward完美转发了,上面test03可以看到,如果我们这样传右值 string B(Rval);,被调用的函数内部是不会被当成右值处理,而string C(std::forward(Rval));后,就肯定是,原来是右值的就当右值使用,原来是左值的就当左值使用,避免被调用函数内部不必要的拷贝,实现完美转发。
或者换句话说,在std::move获取右值引用后传入而非以匿名变量传入,std::forward就是把std::move存在的缺陷补全了,因为获取右值引用后传入而非以匿名变量传入在被调用函数内部被转成了左值拷贝,而std::forward不会。
不过两者一般是配合使用,首先std::move获取右值,然后通过std::forward进行完美转发,这样就可以实现完美转发了。

参考文章1C++11中std:move()的作用和用法
参考文章2c++11 完美转发 std::forward()

四 源码剖析

在理解上面的用法后,我们往更深的层次出发,就是阅读源码。源码参考文章:C/C++学习记录:std::move 源码分析
这里std::move的源码很简单:

template<typename _Tp>
  constexpr typename std::remove_reference<_Tp>::type&&
  move(_Tp&& __t) noexcept
  { return static_cast<typename std::remove_reference<_Tp>::type&&>(__t); }

/// remove_reference
template<typename _Tp>
  struct remove_reference
  { typedef _Tp   type; };

template<typename _Tp>
  struct remove_reference<_Tp&>
  { typedef _Tp   type; };

template<typename _Tp>
  struct remove_reference<_Tp&&>
  { typedef _Tp   type; };

要看懂上面的源码,需要理解几个知识点:

  • 1)_Tp:如果用户没有显示调用模板参数 _Tp ,那么模板会根据用户传进的实参自动推导;如果用户订显示调用模板参数,那么 _Tp 就是用户传进的模板参数, _Tp 的值可能是左值引用或者是右值引用。例如下面是 std::move 显示调用模板参数的例子:
string A("abc");
string &V1 = A;
string r1 = std::move<string&>(V1);                 // 左值做参数, 显示调用模板参数,等价于写成隐式调用模板参数string r1 = std::move(V1); 注意模板参数<string&>与实参V1的类型一定要对应
cout << "A: " << A << endl;                         // output: 空字符串,因为A的引用是V1,而V1的所有权给了r1,所以是空
cout << "r1: " << r1 << endl;                       // output: abc

auto s = std::move<string&&>(std::move(r1));        // 右值做参数。等价于写成:auto s = std::move(std::move(r1)); 或者auto s = std::move<string>(std::move(r1));   
cout << "s: " << s << endl;                         // output: abc
  • 2)而type的作用是:不管你_Tp是左值引用还是右值引用,我只需要拿到你的数据类型即可。例如_Tp传进来是int&或者是int&&,那么type最终的值都是typedef int type,即type都是为int。
    std::remove_reference<_Tp>模板函数的设计思想类似下面的例子,_Tp的类型被推导出来后,作为std::remove_reference的模板参数传入,相当于下面test01的实参,编译器会根据这个实参自动选择对应的std::remove_reference重载函数去处理。例如_Tp=int&,那么就走template struct remove_reference<_Tp&> { typedef _Tp type; };右值引用同理。
int add(int a, int b){
    return a + b;
}

double add(double a, double b){
    return a + b;
}

template<typename T>
T add(T a, T b){
    return a + b;
}

// 编译器选择哪个函数调用的原则:
// 1)若用户显示调用模板参数,如果存在该类型的普通函数,那么就调用该函数,如果不存在该类型的普通函数,那么编译器在编译时会利用模板去生成该类型的函数,给用户调用。
// 2)若用户隐式调用模板参数,会先根据实参推导出要调用函数的类型,如果存在该类型的普通函数,那么就调用该函数,如果不存在该类型的普通函数,那么编译器在编译时会利用模板去生成该类型的函数,给用户调用。
// 例如下面的test01都是隐式调用(针对模板函数的叫法),因为add(1,2)存在有add(int,int)的普通函数,所以会调用它。
// add(1.0, 2.0);调用double的同理;而add(a,b)因为不存在float类型的普通函数,所以在编译时会生成该类型的函数,所以add(a,b)会调用到模板函数。
void test01(){
    add(1, 2);              // 调用int的add
    add(1.0, 2.0);          // 调用double的add
    float a = 2.0, b = 3.0;
    add(a, b);              // 调用模板的add
}
  • 3)这里看到,move只有一个参数,就是_Tp&& __t,这种_Tp&&用在模板参数时,是一种万能引用,知识点称之为引用折叠,它可以接收左值引用或者右值引用,与平常的函数不一样,平常的函数(非模板函数或者非模板类)如果参数是_Tp &&,那么它只能接收右值引用。例如:
// 只能传左值引用
void print1(int &t)
{
}

// 只能传右值引用
void print2(int &&t)
{
}

// 模板加右值引用:此时编译器认为是万能引用,相当于void*。
template<typename T>
void print(T &&t)
{
}

关于引用折叠的知识可以参考:c++引用折叠

  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 4
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值