C++11之引用

目的

了解对应左值引用, 右值引用,移动语义, 完美转发含义。

右值引用(及其支持的移动语义Move semantics和完美转发Perfect forwarding)是C++11中加入的最重大语言特性之一。

右值引用

我希望这篇博客能让我或者读者能够明白如下三点, 那么这篇博客就有意义了。

1. 为什么要引入右值引用这个概念呢?
2. 什么是右值引用
3. 右值引用作用

先引入左值, 右值概念:

   一般化左值(glvalue): 可寻址的表达式,即可使用&操作符的表达式
   纯右值(prvalue): 只读表达式,即不可使用&操作符的表达式,一般为临时值
   xvalue: 延长了生命周期的表达式,即右值引用
        lvalue :等号左边的值称为左值
        rvalue :等号右边的值称为右值
  左值右值是指一个表达式(当然这个表达式可以仅是一个简单的变量),区分左值和右值是看表达式能否使用&操作符。
  左值引用和右值引用是指左值或者右值的型别,左值的型别可以为左值引用,也可以为右值引用。右值的型别可以为右值引用,但不能为左值引用,因为左值引用仅能引用左值(const修饰的左值引用可以引用右值,因为函数参数重const修饰的参数,编译器会为传入的右值创建临时变量,所以const修饰的左值引用其实是引用的这个临时变量)。右值引用仅能引用纯右值。

为啥要引入右值引用

在C++98中,临时量(术语为右值,因其出现在赋值表达式的右边)可以被传给函数,但只能被接受为const &类型。
这样函数便无法区分传给const &的是真实的右值还是常规变量。而且,由于类型为const &,函数也无法改变所传对象的值。
C++0x将增加一种名为右值引用的新的引用类型,记作typename &&。这种类型可以被接受为非const值,从而允许改变其值。这种改变将允许某些对象创建转移语义。比如,一个std::vector,就其内部实现而言,是一个C式数组的封装。如果需要创建vector临时量或者从函数中返回vector,那就只能通过创建一个新的vector并拷贝所有存于右值中的数据来存储数据。之后这个临时的vector则会被销毁,同时删除其包含的数据。有了右值引用,一个参数为指向某个vector的右值引用的std::vector的转移构造器就能够简单地将该右值中C式数组的指针复制到新的vector,然后将该右值清空。这里没有数组拷贝,并且销毁被清空的右值也不会销毁保存数据的内存。返回vector的函数现在只需要返回一个std::vector<>&&。如果vector没有转移构造器,那么结果会像以前一样:用std::vector<> &参数调用它的拷贝构造器。如果vector确实具有转移构造器,那么转移构造器就会被调用,从而避免大量的内存分配。

eg:

std::move();
bool is_r_value(int &&)
{
    return true;
}
bool is_r_value(const int &)
{
    return false;
}
void test(int &&i)
{
    is_r_value(i); // false
    is_r_value(std::move(i)); // true
}

什么是右值引用

最直观理解方法:绑定到右值的引用,哪怕右值是一个临时变量, 只不过让其生命周期变长而已,但是它本身却不能绑定任何左值。

右值引用作用

对于返回右值引用的函数来说,支持右值声明的绑定,不支持非常量左值,却支持非常量左值
引用类型
右值引用

1: 右值引用无论作为参数还是返回值,都可以使用临时变量,并且由于其可以窃取临时变量中的内存,导致其效率较高;
2: 常量左值引用是万能类型,当参数是常量左值时,我们传入右值也可以;当返回值是右值时,使用常量左值也可以接收。
3: 左值引用无论是作为参数还是返回值,都要求其不能使用临时变量。
4:当右值引用作为构造函数参数时,这就是所谓的移动构造函数,也就是所谓的移动语义。

当成员存在指针成员,使用复制拷贝构造函数, 需要进行深拷贝。

snippets. 1:

QString s;
QString p = s;

上面无疑是需要深拷贝的,因为无论s,还是p,都可能在我们后面的代码里面继续用到。

snippets. 2:

QString GetTemp() {return QString("Hello World!");}
int main()
{
	QString str = GetTemp();
}

这里代码中实际只用到了str,但是实际上却调用了一次构造(GetTemp函数中调用String构造生成临时对象)、两次拷贝构造(一次是GetTemp函数调用拷贝构造生成临时对象用于返回、一次是str接收)、三次析构。这里拷贝构造调用了两次深拷贝,但是最后实际使用到的对象却只有str,因此,可以看出,这里有一次深拷贝是多余的。

当堆内存很大时,多余的深拷贝以及其对象的堆内存析构耗时就会变的很可观,那么是否有一种方式,可以让函数中的返回的临时对象空间是否可以不析构,而可以重用呢?

基于上述原因,因此c++11提供了移动构造来解决上述问题。移动构造也是基于右值引用来实现的。

移动构造函数

class MyClass{
public:
	MyClass():d(new int(3)){
	}
	MyClass(const MyClass& h) : d(new int(*h.d)){
	}
	######move constructor
	MyClass(MyClass&& h) : d(h.d){   
	   h.d = nullptr;
	}
private:
	int *d = nullptr;
}

MyClass GetTempClass() {
	MyClass myclass;
	return myclass;
}

int main()
{
	MyClass a = GetTempClass();
	...
}

######move constructor 表示构造函数, 它与拷贝构造函数不同的是,它接收的是一个右值引用的参数,即MyClass && h,移动构造函数使用参数h的成员d初始化了本对象的成员d初始化了本对象的成员d(而不是像构造函数一样需要分配内存,然后再将内容一次拷贝到新分配的内存中),而h的成员d随后就被置空。

这里的“偷”堆内存,就是指将对象d指向h.d所指的内存这一条,除此之外,我们还要讲h的d置为空指针,这是因为再移动构造以后,临时对象会被析构,如果不改变h.d的指向的话,那么我们“偷”来的堆内存也被析构掉了。

那么移动构造函数什么时候才会被触发呢?事实上,我们也提供了拷贝构造函数,从外部调用形式来看,拷贝构造及移动构造调用没有分别,那么怎么确保我们调用的是移动构造呢?这就涉及到临时对象的问题。这里涉及到移动语义了。

移动语义 std::move

std::move主要用于将左值强行转换为右值,需要注意的是,被转化的左值生命周期并没有因这种转换而改变。但是在使用std::move时,我们却需要注意:一旦该左值被转换为右值,如果和移动语义结合使用,那么该左值的生命周期就将结束,如果此后还继续使用改左值,那么就会出现严重错误。

class MyClass{
public:
	MyClass():d(new int(3)){
	}
	MyClass(const MyClass& h) : d(new int(*h.d)){
	}
	MyClass(MyClass&& h) : d(h.d){   
	   h.d = nullptr;
	}
private:
	int *d = nullptr;
}

int main()
{
	MyClass a;
	MyClass c(move(a));
	...
}

如上式中,a由于移动语义,其堆内存实际已被释放,后面继续调用,那么就会报错。
基于此,所以我们应该注意:应当确保使用std::move用于移动语义的变量是一个临时量。下面是把std::move用于移动语义的正确姿势:

Class Moveable {
public:
	...
	Moveable (Moveable &&m) :
		i(m.i) ,h(move(m.h)){ //#1
		m.i = nullptr;
	}
	
int *i;
MyClass h;
}
Moveable GetTemp(){return Moveable();}

Moveable a(GetTemp());

分析上述代码可以发现,GetTemp()临时对象将很快析构,可以避免出现错误。
  
这里考虑一下,如果#1所在的地方HugeMem不支持移动语义怎么办,这也没多大问题,因为此时会调用其常量左值拷贝函数(上文中已经说明了常量左值是接收右值的),因此也不会有多大问题。基于此,因此我们在编写移动构造函数时应总是将拥有堆内存、文件句柄的资源从左值转换为右值。

移动语义与std::move结合时,要格外注意不要误用,下面是一个错误使用的示例:

int main()
{
    Moveable a;
    Moveable c(move(a)); 
    cout << *a.i << endl;
    return 0;
}

a本身是一个左值,但是被move强转为右值,但是a的生命周期又还没有结束,根据上述移动语义的说明,我们可知:a指向i的内存已经被c窃取了,a.i指针指向空,那么一旦输出i的值,那么程序就会出现错误。
   从上面示例我们可以得到一个注意事项,即:我们在使用move语义时,一定要确保被强转的左值很快会被析构,否则就会带来隐患。

移动语义注意事项

移动构造函数中要避免使用const右值引用,因为我们最终是要修改右值引用中堆内存指向的。
C++11中,实际拷贝/移动构造函数有以下三个版本:
    T Object(T &)
    T Object(const T &)
    T Object(T &&)
  一般来说,编译器会隐式的生成一个移动构造函数,不过如果我们自己声明了自定义的拷贝构造函数、拷贝赋值函数、移动赋值函数、析构函数中的一个或多个,那么编译器都不会再生成默认版本。默认版本的移动构造一般也是按位拷贝,这对实现移动语义来说是不够的,通常情况下,如果要实现移动语义,都需要我们自定义移动构造函数。当然,如果类中不包含堆内存,实不实现移动语义都不重要。
  考虑到常量的左值引用是万能的,假设我们传入参数类型为右值,但是又没有实现移动语义会怎么样呢?那么就会进入常量拷贝构造函数,这就确保了即使移动构造不成,还可以拷贝。

移动语义的swap

template<class T>
void swap(T& a,T& b)
{
	T tmp(move(a));
	a = move(b);
	b = move(tmp);
}

上述代码完全避免了资源的释放与申请,从而完成高效置换。

完美转发

所谓完美转发,就是指在函数模板中,完全依照模板的参数的类型,将参数传递给函数模板中调用的另一个函数。由于拷贝问题的存在,所以完美转发一般不包括值传递。
如何确定转发函数的提供的实际类型呢?

template<typename T>
void IamForwording(T &&t){ IrunCodeActually(t); }
T a;
IamForwording(a);//a是左值,而转发函数参数又是右值,此时目标函数IrunCodeActually中的是左值还是右值?

基于上述原因,所以c++11提供了引用折叠,引用折叠一方面确定了左值右值类型叠加时的类型确定规则,另一方面该规则确保了转发者与接收者的类型一致。

可以用两条语句来抽象表示转发者与接收者的参数类型叠加问题:

typedef T& TR;
TR& v;

C++11定义了以下的引用折叠规则:
在这里插入图片描述
 我们可以把TR认为是转发函数参数类型,v为接收函数类型,v的实际类型为叠加后的类型。从上表可以看出一旦定义中出现了左值引用,那么引用这得优先将其折叠为左值引用。

从上表也可以看出,除了第5种(TR 为 T&&,v为TR&, 而v实际为A&)类型外,其他都是不需要进行额外转发就能够确保模板参数类型TR和目标函数参数类型v一致。可见引用折叠规则独自无法完成完美转发。因此,C++11在此基础上又提出了std::forward。
  
 分析上表第5种情况可以看出,实际类型为A&,但是我们传入的是T&&,要确保目标函数也收到T&&,那么就只能A&转换为T&&,很明显,这是左右值的转换,我们很自然想起了std::move,但是c++11为了在功能上区别完美转发,所以使用std::forward取代std::move。

void RunCode(int && m) {}
void RunCode(int &m) {}
void RunCode(const int && m) {}
void RunCode(const int & m) {}

template<typename T>
void PerfectForward(T &&t){RunCode(forward<T>(t));}

int main()
{
    int a;
    int b;
    const int c = 1;
    const int d = 0;

    PerfectForward(a);  // lvalue ref
    PerfectForward(move(b));  // rvalue ref
    PerfectForward(c);  // const lvalue ref
    PerfectForward(move(d)); // const rvalue ref
}

从上面代码种可以看到,当模板类型为左值时,其进入了目标函数的左值版本,当模板类型为右值时,其进入了目标函数的右值版本,转发函数可以视作不存在,这就是完美转发。

博客

博客一
博客二

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

道阻且长,行则降至

无聊,打赏求刺激而已

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值