Perfect Forwarding(完美转发)

完美转发的实现要依赖于模版类型推导和引用折叠和万能引用。

1. 引用折叠

  1. 在C++中,“引用的引用"是非法的。像auto& & rx = x;(两个引用之间有空格),这种直接定义引用的引用是不合法的,但是在编译器通过类型别名或模版类型推导等语境中,可能间接定义出"引用 的引用”,这是引用会形成"折叠"
  2. 引用折叠会发生的场景:模版的实例化,auto类型推导,创建和运用typedef 和别名声明以及decltype语境

引用折叠规则:

所有的引用折叠最终都代表一个引用,要么是左值引用,要么是右值引用。

规则就是:
如果任一引用为左值引用,则结果为左值引用。否则(即两个都是右值引用),结果为右值引用。
-----根本原因是:在一处的声明为左值,就说明该对象为持久对象,编译器就必须此对象可靠(左值)
                                                                         《Effective Modern C++》
x&& && 折叠为 x&& 
x& &,x&& &, x& && 都折叠为 x&

编程实验:

class widget
{};

template<typename T>
void func(T&& param)
{}

widget widgetFactory()
{
    return widget();
}

template<typename T>
class Foo
{
public:
typedef T&& RvalueRefToT;
};

int main()
{
    int x = 0;
    int& rx = x;

    //折叠引用发生的场景1.模版模版实例化
    widget w1;
    func(w1);		//w1为左值,T被推导为widget& , 代入得void func(Widget& && param);
    //引用折叠后得:void func(Widget& param)
    func(widgetFactory());	//传入右值,T被推导为widget,代入的得void func(Widget&& param)
    //这里没有发生引用折叠

    //折叠引用发生的场景2.---auto类型推导
    auto&& w2 = w1;	//w1为左值auto被推导为widget&,代入得:widget& &&, 引用折叠后:widget& w2;
    auto&& w3 = widgetFactory(); //函数返回widget,为右值,auto被推导为widget,代入的: widget w3;

    //引用折叠发生的语境3——tyedef和using
    Foo<int&> f;	//T被推导为int& ,代入得:typedef int& && RvalueRefToT,引用折叠后:typedef int& RvalueRefToT

    //引用折叠发生的语境4——decltype
    decltype(x) && var1 = 10;	//由于x是int类型: 代入得 int&& var1;
    decltype(rx) && var2 = x;	//由于rx为int& 类型,代入得 int& var2;
    return 0;
}

2. 万能引用

万能引用:既可以接受左值类型的参数又可以接受右值类型的参数。

T&&含义:

  1. 当T是一个具体的类型时,T&&表示右值引用,只能绑定到右值。

  2. 当涉及T类型推导时,T&&为万能引用。若用右值初始化万能引用,则T&&为右值引用。若用左值初始化万能引用,则T&&为左值引

条件:T&&是万能引用的两个条件:

(1)必须涉及类型推导

(2)声明的形式也必须正好形如“T&&”。并且该形式被限定死了,任何对其修饰都将剥夺T&&成为万能引用的资格

template<typename T>
ReturnType Function(T&& parem)
{
    // 函数功能实现
}

编程实验:

#include <iostream>
using namespace std;

void fun(int& x) { cout << "call lvalue ref" << endl; }
void fun(int&& x) { cout << "call rvalue ref" << endl; }
void fun(const int& x) { cout << "call const lvalue ref" << endl; }
void fun(const int&& x) { cout << "call const rvalue ref" << endl; }

template<typename T>
void PerfectForward(T&& t)
{
	std::cout << "T is a ref type?: " << std::is_reference<T>::value << std::endl;
	std::cout << "T is a lvalue ref type?: " << std::is_lvalue_reference<T>::value << std::endl;
	std::cout << "T is a rvalue ref type?: " << std::is_rvalue_reference<T>::value << std::endl;

	fun(forward<T>(t));
}

int main()
{
	PerfectForward(10);           // call rvalue ref

	int a = 5;
	PerfectForward(a);            // call lvalue ref
	PerfectForward(move(a));      // call rvalue ref

	const int b = 8;
	PerfectForward(b);           // call const lvalue ref
	PerfectForward(move(b));     // call const rvalue ref

	system("pause");
	return 0;
}

/*输出结果
T is a ref type?: 0
T is a lvalue ref type?: 0
T is a rvalue ref type?: 0 //万能引用绑定右值,T会被推导为 T 类型
call rvalue ref

T is a ref type?: 1
T is a lvalue ref type?: 1
T is a rvalue ref type?: 0	//万能引用绑定左值,T会被推导为T& 类型
call lvalue ref

T is a ref type?: 0
T is a lvalue ref type?: 0
T is a rvalue ref type?: 0
call rvalue ref

T is a ref type?: 1
T is a lvalue ref type?: 1
T is a rvalue ref type?: 0
call const lvalue ref

T is a ref type?: 0
T is a lvalue ref type?: 0
T is a rvalue ref type?: 0
call const rvalue ref
*/

利用引用折叠进行万能引用初始化类型推导

  1. 当万能引用(T&& param )绑定到左值时,由于万能引用也是引用,而左值只能绑定到左值引用,因此,T会被推导为T& 类型,从而 param 的类型为 T& && ,引用折叠后类型变为: T&;
  2. 当万能引用(T&& param )绑定到左值时,右值只能绑定到右值引用,因此,T会被推导为 T 类型,从而 param 的类型为 T&&(右值引用)。

总结:万能引用就是利用模板推导和引用折叠的相关规则,生成不同的实例化模板来接收传进来的参数。

3. 完美转发

指的是 函数模板可以将自己的参数 “完美” 地转发给内部调用的其它函数 ,不仅能准确地转发参数的值,还能 保证被转发参数的左、右值属性不变

//左值版本
template<class T>
constexpr T&& forward(std::remove_reference_t<T>& arg) noexcept
{
    // forward an lvalue as either an lvalue or an rvalue
    return (static_cast<T&&>(arg));//可能发生引用折叠
}
//右值版本
template<class T>
constexpr T&& forward(std::remove_reference_t<T>&& arg) noexcept
{
    // forward an rvalue as an rvalue
    return (static_cast<T&&>(arg));
}

为什么引入完美转发?

编程实验:

void print(const int& t) //左值版本
{
    cout << "void print(const int& t)" << endl;
}
void print(int&& t)		 //右值版本
{
    cout << "void print(int&& t)" << endl;
}

template<typename T> 
void testForward(T&& parm)
{
    //不完美转发
    print(parm);			//parm 是形参,是左值。调用void print(const int& t),永远调用左值版本	
    print(std::move(parm)); //parm是 右值。调用void print(int&& t),永远调用右值版本
    //完美转发
    print(std::forward<T>(parm));//只有这里才会根据传入parm的实参类型的左右值进行转发
}
int main()
{
    cout << "测试右值:" << endl;
    testForward(1);
    cout << "测试左值:" << endl;
    int x = 1;
    testForward(x);
    return 0;
}


/*输出结果:
测试右值
void print(const int& t)
void print(int&& t)
void print(int&& t)		//完美转发,这里转入的值为1,为右值,调用右值版本的print
测试左值
void print(const int& t)
void print(int&& t)
void print(const int& t)//完美转发,这里转入的值为x,为左值,调用左值版本的print
*/

分析std:forward 实现条件转发的原理:以widget类对象为例

画板

:::info
总结:

  1. 当传递给func函数的实参类型是左值widget时,T被推导为widget&类型,然后forward会实例化为std:forward<widget&>,并返回widget&(左值引用)。
  2. 当传递给func函数的实参类型是右值widget时,T被推导为widget类型,然后forward会实例化为std:forward,并返回widget&&(右值引用,注意:匿名的右值引用是右值)。
  3. std:forward会根据传递给func函数的实参的左/右值类型进行转发,当传递给func函数左值实参时,forward返回左值引用,并将改值转发给process函数,而当传递给func函数右值实参时,forward返回右值引用,并将右值转发给process函数。

:::

3.1对比:std::move and std::forward

比较

  1. move和forward都是仅仅执行强制类型转换的函数,std:move 无条件的将实参强制转换为右值。而std:forward 则仅是在某个特定的条件满足时(转入func函数的实参时右值)才进行强制类型转换。
  2. std:move 并不进行任何移动,std:forward 也不进行任何转发。这两者都在运行期间无所作为,他们都不会再生成任何可执行代码。连一个字节都不会生成。

3.2使用时机

  1. 针对右值引用的最后一次使用实施std:move,针对万能引用的最后一次使用实施std:forward。
  2. 在按值返回的函数中,如果返回的是一个绑定在右值引用或万能引用的对象时,可以实施std:move 或 std:forward。因为如果原始对象是右值,它的值就应被移动到返回值上,而如果是左值,就必须通过复制构造出副本作为返回值。

3.3 返回值优化(RVO)

两个前提条件

  1. 局部对象类型函数返回值类型相同
  2. 返回的就是局部对象本身。(包含局部对象和作为return语句中的临时对象等)。

注意事项

  1. 在RVO的前提条件满足时,要么避免复制,要么会自动调用std::move隐式实施于返回值
  2. 按值传递的函数形,把他们作为函数的返回值时,情况与返回值优化类似,编译器会自动选择第二种处理方案,即返回时将形参转为右值处理
  3. 如果局部变量有资格进行RVO优化,就不要将std:move 或 std:forward用在这些变量上,因为这可能会让返回值丧失优化的机会。

//针对右值引用实施std:move 针对万能引用实施std:forward
class Data {

};
class widget
{
	std::string _name;
	std::shared_ptr<Data> _ptr;
public:
	widget()
	{
		cout << "widget()" << endl;
	}
	//复制构造函数
	widget(const widget& w)
		:_name(w._name)
		, _ptr(w._ptr)
	{
		cout << "widget(const widget& w)" << endl;
	}
	//针对右值引用使用std:move
	widget(widget&& rhs)
		:_name(std::move(rhs._name))
		, _ptr(std::move(rhs._ptr))
	{
		cout << "widget(widget&& w)" << endl;
	}
	//针对万能引用使用std:forward.
	//注意这里使用万能引用来代替两个重载版本:void setName(const string& )和 void setName(string&&)
	//好处是当使用字符串字面量时,万能引用的版本的效率更高,如:w.setName("santa"),此时字符串会被推导为
	//const char (&)类型,然后直接转给setName函数,(可以避免先通过字面量来构造string对象),	
	//并将该类型直接转给name的构造函数,节省了一个构造和释放临时对象的开销,效率更高
	template<class T>
	void setNaem(T&& newname)
	{
		if (newname != _name)//第一次使用newname
		{
			_name = std::forward<T>(newname);//针对万能引用的最后一次实施forward
		}
	}
};

	//2. 按值返回函数
	//2.1 按值返回的是一个绑定到右值引用的对象
	class complex 
	{
		double _x;
		double _y;
	public:
		complex(double x = 0, double y = 0)
			:_x(x)
			,_y(y)
		{}
		complex& operator+=(const complex& rhs)
		{
			_x += rhs._x;
			_y += rhs._y;
			return *this;
		}
	};

	complex operator+(complex&& lhs, complex& rhs)
	{
		lhs += rhs;
		return std::move(lhs);
	}

	//2.2 按值返回绑定到一个万能引用对象
	template<typename T> 
	auto test(T&& t)
	{
		return std::forward<T>(t);//由于t是一个万能引用对象。按值返回时实施std:forward
									//如果对象是一个右值,则被移动到返回值上,如果对象是左值
									//则会被拷贝到返回值。
	}

	//3. RVO优化
	//3.1 返回局部对对象
	widget makewidget()
	{
		widget w;
		return w;	//返回局部对象,瞒足RVO优化的两个条件。为避免复制,会直接在返回值内存上创建w对象。
					//如果改为:std:move(w);由于返回值类型不同,(widget& 返回值类型是 widget)
					//会剥夺RVO优化的机会,就会先创建w对象,在移动给返回值,无形中增加了移动动作。
					//对于这种满足RVO优化条件的,在某些条件下无法避免复制(如:多路返回),编译器仍会
					//将w转为右值,即 return std:move(w) ,而无需显示的std:move!!
	}

	//3.2按值形参作为返回值
	widget makewidget(widget w)//注意这里的w是按值传递的
	{
		//...
		return w;//这里虽然不满足RVO条件,(w是形参,不是函数的局部对象),当时仍会被编译器优化,默认的转为右值

	}
int main()
{
	cout << "1. 针对右值引用实施std::move,针对万能引用实施std::forward" << endl;
	widget w;
	w.setNaem("SantaClaus");
	cout << "2. 按值返回时" << endl;
	auto t1 = test(w);
	auto t2 = test(std::move(w));
	cout << "3. RVO优化" << endl;
	widget w1 = makewidget();	//按值返回局部对象
	widget w2 = makewidget(w1);	//按值返回形参对象
	return 0;
}
//运行结果:
/*
1. 针对右值引用实施std::move,针对万能引用实施std::forward
widget()
2. 按值返回时
widget(const widget& w)
widget(widget&& w)
3. RVO优化
widget()
widget(widget&& w)

widget(const widget& w)
widget(widget&& w)
*/

4. 完美转发失败情况

完美转发失败

  1. 完美转发不仅转发对象,还转发其类型,左右值特征以及是否带有const 或 volation 等修饰词。而完美转发的失败,主要来源于模版类型推导失败或结果是错误的类型。
  2. 实例说明:假设转发的目标函数f,而转发函数fwd(天然就应该是泛型)
template<typename... Ts>
void fwd(Ts&&... param)
{
	f(std::forward<Ts>(param)...);
}

f(experssion);	//如果本语句执行了某操作
fwd(experssion);//而用同一实参调用fwd	则会执行不同操作,则完美转发失败

五种情况

  1. 使用{}初始化列表时

分析:由于转发函数是模版函数,而在模版类型推导中,大括号不能自动推导为std:initializer_list

解决方案:用auto 声明一个局部变量,在将局部变量传递给转发函数。

  1. 0和NULL作空指针时

分析:0或NULL以空指针之名传递给模版时,类型推导的结果是整型,而不是所希望的指针类型。

解决方案:传递nullptr,而非0或NULL。

  1. 仅声明static成员变量而无定义时

分析:C++中常量一般是进入符号表中的只有对其取地址是才会实际分配地址,调用f函数时,其实参是直接从符号表中取值,此时不会出现问题,但当调用fwd由于其形参是万能引用,而引用本质上是可解引用的指针。因此当传入fwd会要求准备某块内存以供解引用出该变量出来,但因其未定义,也就没有实际的内存空间,编译时可能失败。注意:如果是static const 声明的常量的值是整形,或者布尔型的,那么就可以直接在类中进行定义初始化,如果是浮点数和自定义类型,那么就需要再类外进行定义初始化,

解决方案:在类外定义该成员变量。注意着这变量在生声明时,一般会先给初始值,因此在定义时,无需也不能再重复指定初始值

  1. 使用重载函数名或模版函数名时

分析:由于fwd是模版类型参数,其形参没有任何类型的信息。当传入重载函数名或模版函数时(代表许多函数),就会导致fwd的形参不知绑定到哪一个函数上。

解决方案:在调用fwd函数时手动为形参指定类型信息。

  1. 转发位域时

分析:位域是由机器字的若干任意部分组成,(任意int的3 到 5个比特位),但这样的实体是无法取地址的,而fwd的形参是引用,本质就是指针,所以也没有办法创建指向任意比特的指针。

解决方法:制作位域的副本,并以该副本来调用转发函数。

//大括号初始化列表
void f(const std::vector<int>& v)
{
    cout << "void f(const std::vector<int>& v)" << endl;
}
//2. 用0或NULL作空指针时
void f(int x)
{
    cout << "void f(intx)" << endl;
}
//3.仅声明static const的整型变量而无定义
class widget
{
public:
	static const double MinValue ;//仅声明无定义(静态变量需要再类外定义)
};
const double widget::MinValue = 28.10;//在类外定义无需也不能重复定义初始值
//4. 使用重载函数名或模版函数名
int f(int (*pf)(int))
{
    cout << "int f(int (*pf)(int))" << endl;
    return 0;
}
int processVal(int value) 
{
    return 0;
}
int processVal(int value,int priority)
{
    return 0;
}

//5. 位域
struct IPv4Header
{
std::uint32_t version : 4,
IHL : 4,
    DSCP : 6,
    ECN : 2,
    totalLength : 16;
//...
};
template<typename T> 
T workonval(T param)//模版函数代表许多函数
{
    return  param;
}

//用于测试的转发函数
template<typename ...Ts>
void fwd(Ts&& ...param)
{
    f(std::forward<Ts>(param)...);//目标函数
}
int main()
{
    cout << "-------------------1. 大括号初始化列表---------------------" << endl;
    //1.1 用同一实参分别调用f和fwd函数
    f({ 1,2,3 });//{1,2,3}会被隐式转函为vector<int>
    //fwd({ 1,2,3 });//编译失败,由于fwd是模版函数,而模版推导时{}不能被自动推导为std::initializar_list<int>
    //解决方案:
    auto il = { 1,2,3 };
    fwd(il);



    cout << "-------------------2. 0或NULL用作空指针-------------------" << endl;
    //2.1 用同一实参分别调用f和fwd函数
    f(NULL);	//调用void f(int) 函数
    fwd(NULL);	//NULL被推导为int,仍调用void f(int )
    //2.2 解决方案:使用nullptr
    f(nullptr);	//匹配int f(int (*pf)(int ))
    fwd(nullptr);

    cout << "-------3. 仅声明static const的整型成员变量而无定义--------" << endl;
    //3.1 使用同一实参分别调用f和fwd函数
    f(widget::MinValue);	//调用用void f(int) 函数实参从符号表中取得,编译成功
    fwd(widget::MinValue);	//fwd的形参是引用,而引用的本质是指针,但fwd使用到该实参时需要解引用
    //这里会因为没有为MinValue分配内存·而出现编译错误
    //3.2解决方案:在类外定义该变量
    cout << "-------------4. 使用重载函数名或模板函数名---------------" << endl;
    //4.1 使用同一实参分别调用f和fwd函数
    f(processVal);	//由于形参是int(*pf)(int),带有类型信息会匹配int processVal(int value)
    //fwd(processVal);//error,fwd的形参不带任何类型信息,不知到会匹配到哪个processVal函数
    //4.2 解决方案
    using processFuncType = int(*)(int);
    processFuncType processValOPtr = processVal;
    fwd(processValOPtr);
    fwd(static_cast<processFuncType>(workonval));//调用int f((*pf)(int))
    cout << "----------------------5. 转发位域时---------------------" << endl;
    //5.1 用同一实参分别调用f和fwd函数
    IPv4Header ip = {};
    f(ip.totalLength); //调用void f(int )
    //fwd(ip.totalLength);	//error,fwd形参是引用,由于位域是比特位组成的,无法比特位的引用
    //解决方案:创建位域的副本,并传给fwd
    auto length = static_cast<std::uint16_t>(ip.totalLength);
    fwd(length);

    return 0;

}
/*输出结果:
-------------------1. 大括号初始化列表---------------------
void f(const std::vector<int>& v)
void f(const std::vector<int>& v)
-------------------2. 0或NULL用作空指针-------------------
void f(intx)
void f(intx)
int f(int (*pf)(int))
int f(int (*pf)(int))
-------3. 仅声明static const的整型成员变量而无定义--------
void f(intx)
void f(intx)
-------------4. 使用重载函数名或模板函数名---------------
int f(int (*pf)(int))
int f(int (*pf)(int))
int f(int (*pf)(int))
----------------------5. 转发位域时---------------------
void f(intx)
void f(intx)
*/

参考博客:链接: link
链接: link
链接: link
(全文完)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值