C/C++编程:完美转发

所谓完美转发, 是指在函数模板中,完全依照模板的参数类型,将参数传递给函数模板中调用的另一个函数。

引入

看个例子:

template<typename T>
void IamForwording(T t) {
	IrunCodeActually(t);
}

上例中,IamForwording是一个转发函数模板。而函数IrunCodeActually是真正执行代码的目标函IamForwording数。对于IrunCodeActually而言,它总是希望转发函数IamForwording将参数按照传入IamForwording时的类型传递,而不产生额外的开销,就好像转发者不存在一样。

但是上例中,IamForwording的参数会导致参数在传给IrunCodeActually之前产生额外的临时对象拷贝。这样的转发只能说是正确的转发,谈不上完美。

所以通常要做到完美转发,需要传递的是一个引用类型,引用类型不会有额外的开销

其次,要考虑转发函数对类型的接收能力。因为目标函数可能需要即接收左值引用,又接受右值引用。那么如果转发函数只能接受其中的一部分,我们也无法做到完美转发。当然,你可能会想到使用常量左值类型(即能接受左值引用,又能接受右值引用)作为转发函数的类型,但这可能会遇到一些问题:

void IrunCodeActually(int t) {}
template<typename T>
void IamForwording(const T& t) {
	IrunCodeActually(t);
}

这里,由于目标函数的参数类型是非常量的左值引用类型,因此无法接收常量左值引用作为参数(这个问题的可以用重载来解决,但是这可能会造成代码冗余)。同理,当目标函数的参数是个右值引用,同一无法接受任何左值类型作为参数,间接的,也就导致无法使用移动语句。

那C++11是如何解决完美转发的问题呢?实际上,C++是通过引入一条所谓引用折叠的新语言规则,并结合新的模板推导规则来实现完美转发。

我们来看个例子。我们知道,一个声明的右值引用其实是一个左值,这就为我们进行参数转发(传递)造成了问题:

#include <iostream>


void reference(int &v){
    printf("左值\n");
}

void reference(int &&v){
    printf("右值\n");
}

template <typename T>
void pass(T && v){
    printf("普通传参:");
    reference(v);
}

int main(){
    printf("传递右值:");
    pass(1); //1 是右值,输出左值

    printf("传递左值:");
    int l = 1;
    pass(l); // l是左值,输出右值
    return 0;
}

在这里插入图片描述

为什么会是这样的结果呢?

这是基于引用坍缩(折叠)规则(将复杂的未知表达式折叠为已知的简单表达式)的:

  • 在传统C++中,我们不能对一个引用类型继续进行引用。也就是说:
typedef const int T;
typedef T& TR;
TR& v = 1; //C++11之前会编译错误
  • 但是C++由于右值引用的出现而放宽了这一做法,从而产生了引用坍缩规则,允许我们对引用进行引用,即能左引用,又能右引用。但是遵循如下规则:
函数形参类型实参函数类型 推到后函数形参类型
T&左引用
T &右引用
T&&左引用
T&&右引用

因此,模板函数中使用T&&不一定能进行右值引用,当传入左值时,此函数的引用被推导为左值。更准确的讲,无论模板参数是什么类型的引用,当且仅当实参类型为右引用时,模板参数才能被推导为右引用类型。这才使得v作为左值的成功传递。

也就是说,我们可以把转发函数写成如下形式:

template<typename T>
void IamForwording(T &&t){
	IrunCodeActually(static_cast<T &&>(t));
}

从而,如果我们调用转发函数时传入一个X类型的左值引用,转发函数将被实例化为如下形式:

void IamForwording(X& &&t){
	IrunCodeActually(static_cast<X& &&>(t));
}

引用引入坍缩规则,就是:

void IamForwording(X& t){
	IrunCodeActually(static_cast<X& >(t));
}

如果传入一个X类型的右值引用,转发函数将被实例化为如下形式:

void IamForwording(X&& &&t){
	IrunCodeActually(static_cast<X&& &&>(t));
}

引用引入坍缩规则,就是:

void IamForwording(X&& t){
	IrunCodeActually(static_cast<X&& >(t));
}

这里我们就看到了static_cast的重要性。

定义

完美转发就是基于引用坍缩(折叠)规则产生的。

  • 所谓完美转发,就是为了让我们在传递参数的时候,保持原来的参数类型(左引用保持左引用,右引用保持右引用)。
  • 为了解决这个问题,我们应该使用std::forward来进行参数的转发(传递):
#include <iostream>


void reference(int &v){
    printf("左值\n");
}

void reference(int &&v){
    printf("右值\n");
}

template <typename T>
void pass(T && v){
    printf("\t普通传参:");
    reference(v);
    printf("\tstd::move传参:");
    reference(std::move(v));
    printf("\tstd::forward传参:");
    reference(std::forward<T>(v));
    printf("\tstd::static_cast传参:");
    reference(static_cast<T&&>(v));
}

在这里插入图片描述
无论传递参数为左值还是右值,普通传参都会将参数作为左值转发,所以std::move总是会接受到一个左值,从而转发调用了reference(int &&)输出右值调用

唯有std::forward既没有造成任何多余的拷贝,同时完美转发(传递)了函数的实参给了内部调用的其他函数

std::forward和std::move一样。没有做任何事情,std::move单纯的将左值转化为右值,std::forward也只是单纯的将参数做了一个类型的转换,从现象上看,std::forward<T>vstaatic_cast<T&&>v
是完全一样的。

那,为何一条语句能够针对两种类型的返回对应的值,我们来看以下std::forward的具体实现机制,std::forward包含两个重载:

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

template<typename _Tp>
constexpr _Tp&&forward(typename std::remove_reference<_Tp>::type&& __t)noexcept{
	static_assert(!std::is_lvalue_reference<_Tp>::value, "template argument substituting _Tp is an lvalue reference type");
	return static_cast<_Tp&&>(__t);
}

在这份实现中,std::remove_reference的功能是消除类型中的引用,而std::is_lvalue_reference用于检查类型推导是否正确,在std::forward的第二个实现中检查了接收到的值确实是一个左值,进而体现了坍缩规则。

当std::forward接受左值时,_Tp被推导为左值,所以返回值为左值;而当其接受右值时,_Tp被推导为右值引用,则基于坍缩规则,返回值成了&& + &&的右值。可见std::forward的原理在于巧妙的利用了模板类型推导中产生的差异。

这也就能回答”为什么在使用循环语句的过程中,auto&& 是最安全的方式?“:因为当auto被推导为不同的左右引用时,与&&的坍缩组合是完美转发。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值