C++11新特性——右值引用、移动语义和完美转发

一、概念

移动语义:使编译期使用移动操作来替换复制操作。如unique_ptr、future、thread。
完美转发:使得人们可以撰写接受任意实参的函数模板,并将其转发到其他函数,目标函数会接受到与转发函数所接受的完全相同的实参。
右值引用:将移动语义和完美转发胶合起来的底层语言特性。

要点:函数形参总是左值,即使其类型为右值引用:

void f(Widget&& w);

w是个左值,即使定义为右值引用。

二、move和forward

move:无条件将实参强制转化为右值。实例实现如下:

template<typename T>
typename remove_reference<T>::type&& move(T&& param) {
	using ReturnType = typename remove_reference<T>::type&&;
	return static_cast<ReturnType>(param);
}

形参是指向一个对象的引用(万能引用),返回的是指向同一对象的引用。若T是个左值引用,则T&&就成了左值引用,为避免这种情况,使用remove_reference保证&&作用在一个非引用类型上。

可移动的经验:
1、若想取得某个对象执行移动操作的能力,则不要将其声明为常量,因为针对常量对象执行的移动操作将会被变成复制操作。
2、move不移动任何东西,不保证经过其强制类型转换后的对象具备可移动的能力,只能确定使用move之后的结果是个右值。

forward:仅在某个特定条件满足时才执行同一个强制转换。典型用例:

void process(const Widget& lval);//处理左值
void process(Widget&& rval);//处理右值

template<typename T>
void logAndProcess(T&& param) {
	auto now = chrono::system_clock::now();//获取当前时间
	//log  now
	process(forward<T>(param));
}
//调用
Widget w;
logAndProcess(w);//调用时传左值
logAndProcess(move(w));//调用时传右值

当且仅当用来初始化param的实参是个右值的条件下,forward才把param强制转化为右值类型。

在运行期,move和forward都不会做任何操作

三、万能引用和右值引用

1、若函数模板形参具备T&&类型,且T的类型是系统推导而来,或对象使用auto&& 声明类型,则该形参或对象就是个万能引用。

void f(Widget&& param); //右值引用
Widget&& var1=Widget(); //右值引用

auto&& var2=var1; //万能引用

template<typename T>
void f(vector<T>&& param) //右值引用

template<typename T>
void f(T&& param); //万能引用

2、若类型声明并不精确地具备type&&形式,或类型推导并未发生,则type&&代表右值引用。
3、采用右值初始化万能引用则得到右值引用;采用左值初始化万能引用则得到左值引用。

四、右值引用实施move,万能引用实施forward

当转发右值引用给其他函数时,应当对其实施向右值的无条件强制类型转换(move),因为其一定绑定到右值;
当转发万能引用时,应对其实施向右值的有条件强制类型转换(forward),因为其不一定绑定到右值。

class Widget {
public:
	Widget(Widget&& rhs) //右值引用
	:name(move(rhs.name)) {}

	template<typename T>
	void setName(T&& newName) {//万能引用
		name = forward<T>(newName);
	}
private:
	string name;
};

1、针对右值的最后一次使用实施move,针对万能引用的最后一次使用实施forward。
通过左值和右值引用的重载函数来替换使用万能引用形参的函数模板(动机是能使用const修饰函数参数),会带来效率问题,且当函数参数有多个时,重载函数的个数会指数级上升。

class Widget {
public:
	void setName(const string& newName) {//常量左值赋值
		name = newName;
	}
	void setName(string&& newName) {//右值赋值
		name = move(newName);
	}
private:
	string name;
};

2、作为按值返回的函数的右值引用和万能引用,依第1条执行
3、若局部对象可能适用于返回值优化,则勿针对其实施move和forward

Widget makeWidget{
	Widget w;
	...
	return w;
	//return move(w);//不这样做!
}

返回值优化:编译期在一个按值返回的函数里省略对局部变量的复制(或者移动)则需满足两个条件:
1、局部对象类型和函数返回值类型相同
2、返回的就是局部对象本身。

五、万能引用进行重载的问题和替代方案

1、把万能引用作为重载候选类型,几乎总是会让重载版本在始料未及的情况下被调用到。

class Person {
public:
	template<typename T>
	explicit Person(T&& n)
	 :name(forward<T>(n)) {}//完美转发构造函数
	 
	explicit Person(int idx);//形参为int的构造函数

	Person(const Person& rhs);//编译期自动生成的复制构造函数
	Person(Person&& rhs);//编译期自动生成的移动构造函数
};

若传入的是short类型,则也会调用完美转发构造函数。

Person p("aa");
auto cloneP(p);  //调用完美转发构造函数,而非复制构造函数,编译不通过

2、完美转发构造函数的问题尤其严重,对于非常量的左值类型而言,他们都会形成相对于复制构造函数的更加匹配,并且还会劫持派生类中对基类的复制和移动构造函数的调用。

class SpecialPerson :public person {
public:
	SpecialPerson(const SpecialPerson& rhs)
	:Person(rhs){}//复制构造函数调用基类完美转发函数
	
	SpecialPerson(SpecialPerson&& rhs) 
	:Person(move(rhs)) {}//移动构造函数调用基类完美转发函数
};

替代方案:
1、针对函数,可以更改函数名,舍弃重载;但是对象的构造函数则无法实现
2、传递左值常量引用类型(const T&)替代万能引用类型。虽然效率下降,但能解决使用万能引用重载带来的不良效应。
3、把传递的形参从引用型替换成值类型。

class Person {
public:
	explicit Person(string n)
	 :name(move(n)) {}//替换T&&的构造函数
	 
	explicit Person(int idx);//形参为int的构造函数

上述三种方式舍弃了完美转发,但是如果既要保留完美转发又不放弃重载,解决办法如下:
1、标签分派:模板元编程的标准构件。若万能引用仅是形参列表的一部分,该列表中还有其他非万能引用类型的形参,则可进行分派。

template<typename T>
void logAndAdd(T&& name) {
	logAndAddImpl(forward<T>(name), is_integral<typename remove_reference<T>::type>());//typename不能省略,因为remove_reference::type对模板形参T有依赖
}

template<typename T>
void logAndAddImpl(T&& name, false_type)//非整形实参
{
	names.emplace(forward<T>(name));
}

string nameFromIndex(int idx);
void logAndAddImpl(int idx, true_type)//整形实参
{
	logAndAdd(nameFromIndex(idx));//查找名字并调用
}

2、对接受万能引用的模板施加限制(enable_if)

在完美转发构造函数问题中,编译期会自动生成复制和移动构造函数,此时针对构造函数进行标签分派无法解决。

enable_if:强制编译期表现出的行为如同特定的模板不存在一般,默认都是启用的。本例中,仅在传递给完美转发构造函数的类型不是Person时启用它,当传递的是Person则禁用,使得类的默认复制或移动函数能够匹配上。

class Person {
public:
	template<typename T,typename=typename enable_if<consition>::type>
	explicit Person(T&& n);
};

其中的condition为判断两个类型是否统一,即是不是都为Person。!is_same::value几乎实现这一目标,但是针对:

Person p("aa");
auto cloneP(p);

使用左值初始化万能引用时,T总被推导为左值引用。此时T被推导为Person&,不符合要求。因此在审查T时应忽略:
1、它是否是一个引用。为判定万能引用构造函数是否应该被启用,类型Person、Person&和Person&&都应该和Person作相同处理。
2、它是否带有const和volatile修饰词。const Person、volatile Person、const volatile Person都应该和Person作相同处理

decay实现了移除T带有的所以引用、const、volatile修饰词。
此时condition为:
!is_same::value

此外,遇到继承问题时,问题依然存在。标准库有一个类型特征(is_base_of)用来判断一个类型是否是另一个类型派生而来的。此时用is_base_of替换is_same。condition为:
!id_base_of::value

最后,若在构造函数中也需要区分整型和非整形,即标签分派,则需要1、为Person添加一个处理整型实参的构造函数重载版本。2、进一步限制构造函数。最终的代码如下:

class Person {
public:
	template<
		typename T,
		typename=typename enable_if<
		!is_base_of<Person,typename decay<T>::type>::value
		&& !is_integral<remove_reference<T>>::value>>
	explicit Person(T&& n);
};

万能引用形参在性能上具备优势,但在易用性上一般会有劣势(内部类型推导和转化很复杂)。

六、引用折叠

如果引用出现在允许的语境(4种),双重引用则会折叠成单个引用:如果任一引用为左值引用,则结果为左值引用。否则为右值引用。

int x=0;//x是一个左值变量
auto && rx=x;//auto被推导为int& 此时为int& && rx=x;折叠之后为int& rx=x;

万能引用并非一种新的引用类型,他就是满足两个条件语境的右值引用:
1、类别推导的过程中会区别左值和右值。T类型的左值推导为T&,T类型的右值推导为T。
2、会发生引用折叠。

引用折叠的四个语境:
1、模板实例化(forward完美转发左值时,会有引用折叠)
2、auto类型生成
3、创建和运用typedef和别名声明(模板中内嵌一个右值引用类型的typedef)

template<typename T>
class Widget{
public:
	typedef T&& RvalRefToT;
};

当以左值引用类型实例化时:
Widget w;
此时T推导为int&,代入类中如下:
typedef int& && RvalRefToT;
引用折叠为:
typedef int& RvalRefToT;

4、decltype的使用中过程中,出现引用的引用,则引用折叠会出现。

七、完美转发的失败

完美转发的含义:将一个函数的形参传递(转发)到另一个函数,不仅转发对象,还转发显著特征(类型、左值还是右值、是否带const或volatile修饰词等)。

实例代码:

template<typename T>
void fwd(T&& param)
{
	f(forward<T>(param));
}

f(expression);
fwd(expression);
给定目标函数f和转发函数fwd,当以某特定实参调用f会执行某操作,而当同一实参调用fwd会执行不同的操作,则完美转发失败。

不能实施完美转发的情况:
1、大括号初始物。

假设f声明如下:

void f(const vector<int>& v);

此时:

f({1,2,3});//编译通过。编译期先接受调用端的实参类型({1,2,3}),和f声明的形参类型(const vector&)进行比较看是否兼容,此外,在一些情况下甚至会调用隐式类型转换来保证成果

fwd({1,2,3})//编译失败。编译期此时采用推导的手法来取得传递给fwd实参的类型结果。此时推导为initializer_list,而fwd形参并未声明为initializer_list。

auto il={1,2,3};//auto在以大括号初始化物完成初始化时,类型推导 成功
fwd(il);//编译通过,此时il被完美转发给f函数

完美转发在下面两个条件的任何一个成立时,会失败:
1、编译期无法为一个或多个fwd的形参推导出类型结果。此情况下,代码无法编译通过。
2、编译期为一个或多个fwd形参推导出来错误的类型结果。错误的原因:fwd根据类型推导结果的实例化无法编译通过,fwd推导得到的类型调用f与直接以传递个fwd的实参调用f行为不一致(重载版本调用有误)。

2、0和NULL用作空指针
尝试将0或NULL以空指针之名传递给模板,类型推导结果会是整数,一般是int,而非指针类型。因此0和NULL都不能用作空指针进行完美转发。修正方案就是使用nullptr

3、仅有声明的整型static const成员变量

class Widget{
public:
	static const size_t MinVals=28;
};

此时f定义为:

void f(size_t val);

f(Widget::MinVals);//编译通过。当f(28)处理
fwd(Widget::MinVals)//能过编译,无法链接。在对MinVals取址时,MinVals必须得有一块内存供指针指向,而实际上MinVals由于只声明未定义,没有分配内存。

可以在cpp文件中定义该变量解决上述问题:

const size_t Widget::MinVals;

4、重载的函数名字和模板名字

void f(int pf(int));

int processVal(int value);
int processVal(int value,int priority);

f(processval);//通过,匹配f形参的那个,接受int版本的processVal,将地址传给f
fwd(processVal);//失败。processVal无类型。

虽然可以通过手动指定需要转发的重载版本,但是完美转发是被用来接受任何类型的,无法知道要传递的类型。

5、位域:

struct IPV4Header//IPV4头部
{
	uint32_t version : 4,
		IHL : 4,
		DSCP : 6,
		ECN : 2,
		totalLength : 16;
};

void f(size_t sz);
IPV4Header h;
f(h.totalLength);//通过
fwd(h.totalLength);//错误

fwd的形参是个引用,而h.totalLength是个非const的位域。C++规定“非const引用不得绑定到位域”。没有办法创建指向任意比特的指针,因此无法将引用绑定到任意比特。

改进的方法:利用转发目的函数接受的总是位域值的副本,自己制作一个副本,用副本调用函数
形参按值:被调用函数收到的是位域内的值的副本。
常量引用传递:引用实际绑定到存储在某标准整型的位域值的副本。

auto length=static_const<uint16_t>(h.totalLength);//创建副本
fwd(length);//转发副本

你可能感兴趣的:(C++11新特性,c++,内存泄漏,c++11)