【移动语义和完美转发】

一、背景知识

左值:左值的英文简写为“lvalue”。lvalue 是“loactor value”的缩写,表示存储在内存中、有明确存储地址(可寻址)的数据。一般来说,有名称的非临时变量叫做左值
右值:左值的英文简写为“rvalue”。rvalue 是“read value”的缩写,指的是那些可以提供数据值的数据(不一定可以寻址,例如存储于寄存器中的数据)。一般来说,没名称的临时变量叫做右值

引用折叠:编译器不允许我们写下类似int & &&这样的代码,但是它自己却可以推导出int & &&代码出来。它的理由就是:我(编译器)虽然推导出T为int&,但是我在最终生成的代码中,利用引用折叠规则,将int & &&等价生成了int &。推导出来的int & &&只是过渡阶段,最终版本并不存在,所以也不算破坏规定。引用折叠的规则如下:
在这里插入图片描述

万能引用:对于函数模板中使用右值引用的参数来说,它既可以接收右值,也可以接收左值,这个情况下的右值引用也称为万能引用。
浅拷贝:创建一个新对象。如果原始对象属性是基本类型,拷贝的就是基本类型的值;如果原始对象属性是引用类型,拷贝的就是内存地址。这个内存地址指向同一个堆内存。如果其中一个对象改变了这个地址,就会影响到另一个对象。 一般情况下,浅拷贝没有任何副作用,但是当类中有指针,并且指针指向动态分配的内存空间,析构函数做了动态内存释放的处理,会导致内存问题。
浅拷贝
深拷贝:创建一个新对象,如果原始对象属性是基本类型,拷贝的就是基本类型的值;如果原始对象属性是引用类型,则从堆内存中开辟一个新的区域存放该引用类型指向的堆内存中的值,修改新对象的值不会影响原对象。
深拷贝
拷贝构造函数:在未定义拷贝构造函数的情况下,系统会调用默认的拷贝函数——即浅拷贝(不用自己构造)。它能够完成成员的简单的值的拷贝。当数据成员中没有指针时,浅拷贝是可行的;但当数据成员中有指针且指向堆区时,如果采用简单的浅拷贝,则两类中的两个指针将指向同一个地址,当对象快结束时,会调用两次析构函数,而导致指针悬挂现象,所以,此时必须采取深拷贝的形式构造拷贝构造函数。

二、移动语义

移动语义:所谓移动语义,指的就是以移动而非深拷贝的方式初始化含有指针成员的类对象。简单的理解,移动语义指的就是将其他对象(通常是临时对象)拥有的内存资源“移为已用”。换句话说,就是以浅拷贝的方式复制指针,然后将原指针置为空指针。移动构造函数就是通过移动语义的方式来初始化对象的。

#include <iostream>
using namespace std;

class demo{
public:
   demo():num(new int(0)){
      cout<<"construct!"<<endl;
   }
   //拷贝构造函数
   demo(const demo &d):num(new int(*d.num)){
      cout<<"copy construct!"<<endl;
   }
   ~demo(){
      cout<<"class destruct!"<<endl;
   }
private:
   int *num;
};
demo get_demo(){
    return demo();
}
int main(){
    demo a = get_demo();
    return 0;
}

以上面这个例子为例,当我们执行demo a = get_demo()这个语句时,实际上是调用了两次拷贝构造函数,首先第一次调用拷贝构造函数将返回值拷贝给一个临时变量,然后再对临时变量和变量a调用一次拷贝构造函数。因为目前多数编译器都会对程序中发生的拷贝操作进行优化,因此如果我们使用 VS2019等这些编译器运行此程序时,看到的往往是优化后的输出结果:

construct!
class destruct!

而同样的程序,如果在 Linux 上使用g++ demo.cpp -fno-elide-constructors命令运行(其中 demo.cpp 是程序文件的名称),就可以看到完整的输出结果:

construct!            <-- 执行demo()
copy construct!       <--demo()返回的临时对象拷贝给get_demo()返回的临时对象
class destruct!       <-- 析构demo()返回的临时对象
copy construct!       <--get_demo()返回的临时对象拷贝给a
class destruct!       <-- 销毁get_demo()返回的临时对象
class destruct!       <-- 销毁 a

当把程序中的拷贝构造函数替换为如下移动构造函数后,num 指针变量采用的是浅拷贝的复制方式,同时在函数内部重置了 d.num,有效避免了“同一块对空间被释放多次”情况的发生。

   //移动构造函数
    demo(demo &&d):num(d.num){
        d.num = NULL;
        cout<<"move construct!"<<endl;
    }

三、完美转发

完美转发:完美转发就是一个能根据参数类型调用相应函数的接口,当参数是左值引用时,调用相应的函数且传递的参数是左值引用;当参数是右值引用时,调用相应的函数且传递的参数是右值引用。

#include <iostream>
using namespace std;

// 接收左值的函数 f()
template<typename T>
void f(T &)
{
	cout << "f(T &)" << endl;
}

// 接收右值的函数f()
template<typename T>
void f(T &&)
{
	cout << "f(T &&)" << endl;
}

// 万能引用,转发接收到的参数 param
template<typename T>
void PrintType(T&& param)
{
	f(param);  // 将参数param转发给函数 void f()
}

int main()
{
	int a = 0;
	PrintType(a);//传入左值
	PrintType(0);//传入右值
}

我们执行上面的代码,按照预想,在main中我们给 PrintType 分别传入一个左值和一个右值。PrintType将参数转发给 f() 函数。f()有两个重载,分别接收左值和右值。正常的情况下,应该打印f(T&)和f(T&&)。但真实情况确实

f(T &);
f(T &);

因为具名变量即使被声明为右值类型也不会被当作右值,所以当外部传入参数给 PrintType 函数时,param一定是一个左值。大家只需要己住,任何的函数内部,对形参的直接使用,都是按照左值进行的。因为param 是左值,使用这个参数时还是会调用拷贝构造函数。
考虑到模板中的 T 保存着传递进来的实参的信息,我们可以利用 T 的信息来强制类型转换我们的 param 使它和实参的类型一致。具体的做法就是,将模板函数修改如下:

template<typename T>
void PrintType(T&& param)
{
	f(std::forward<T>(param)); 
}

四、小结

综上,当没有定义拷贝构造函数时,初始化对象会调用默认拷贝构造函数–以浅拷贝的方式;当定义拷贝构造函数,初始化对象会调用拷贝构造函数–以深拷贝的方式。
移动构造函数的参数是右值引用,右值初始化后一般就没用了,因为移动构造函数会占有其他对象的内存资源,所以右值调用移动构造函数比较合适;拷贝构造函数的参数是左值引用,左值初始化后一般还有用,而拷贝构造函数是新申请内存资源。所以左值调用拷贝构造函数比较合适。当一个对象是左值,但是初始化其他对象后就没用了,你也可以用std::move()把它变成右值来调用移动构造函数。
当同时存在功能相同,但一个参数时左值引用,一个参数是右值引用的两个函数时,可以利用std::forward< T >()和引用折叠来实现一个接口–当参数是左值引用时,调用相应的函数且传递的参数是左值引用;当参数是右值引用时,调用相应的函数且传递的参数是右值引用,也就是所谓的完美转发。

五、参考文献

https://zhuanlan.zhihu.com/p/50816420

  • 6
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值