一、什么是完美转发
提到完美转发,就有必要说一下,什么是转发,什么样的转发才称得上是完美转发。
在C++中,转发指的就是函数之间的参数传递(例如函数f1
接收了一个参数a
,而后又将此参数a
传递给了其函数体内调用的另一个函数f2
)
而完美转发指的就是在函数之间传递参数的过程中,参数在传递后的属性保持不变(如左值仍是左值,右值仍是右值,const
修饰也会保留)。
二、常规转发存在的问题
对于普通的转发,参数在函数间传递时属性可能会发生改变,我们看一个例子。
#include <iostream>
#include <utility>
void an_orther_fun(int a, int& b)
{
std::cout << "in an_orther_fun(): a = " << a << ", b = " << ++b << std::endl;
}
void transmit(int a, int b)
{
an_orther_fun(a, b);
}
int main()
{
int a = 2, b = 3;
std::cout << " before transmit(): a = " << a << ", b = " << b << std::endl;
transmit(a, b);
std::cout << " after transmit(): a = " << a << ", b = " << b << std::endl;
return 0;
}
其输出为:
before transmit(): a = 2, b = 3
in an_orther_fun(): a = 2, b = 4
after transmit(): a = 2, b = 3
注意,函数an_orther_fun()
的第二个参数类型是引用int&
,并且我们在函数中给该引用的值加上了1(++b
那里),也就是我们预期应当会修改其第二个参数的值加1
。但根据输出,虽然我们在an_orther_fun()
打印出了加1
后的b
的值。但在外层,我们执行tansmit()
后,b
的值并没有加 1。
这里的原因其实很显而易见的,就是我们b
在从transmit()
传递到an_other_fun()
的时候,其属性已经改变了:transmit()
中的b
是外层b
的一个副本,而不是引用。
这就是常规引用可能带来的转发问题。
你可能会说,我们将transmit()
的第二个参数类型也改为引用int&
不就可以解决了吗?
void transmit(int a, int& b) { ... }
在我们这个示例中,这样的修改确实是可以达成我们示例中的目的的。
但,这只是示例,实际还有更多的可能性。例如,如果上例中的b
处是一个右值引用,怎么办?我们看下面的代码:
#include <iostream>
#include <utility>
void an_orther_fun(int a, int&& b)
{
std::cout << "in an_orther_fun(): a = " << a << ", b = " << b << std::endl;
}
void transmit(int a, int&& b)
{
an_orther_fun(a, b); // error: cannot bind rvalue reference of type ‘int&&’ to lvalue of type ‘int’
}
int main()
{
int a = 2;
std::cout << " before transmit(): a = " << a << ", b = " << b << std::endl;
transmit(a, 3);
std::cout << " after transmit(): a = " << a << ", b = " << b << std::endl;
return 0;
}
这里感觉像是
c++
自己埋的坑:想使用生命周期,又没设计好,搞得语法有点模糊,c++里面的编译期模板函数有作用,通过返回右值引用的函数可以将左值转化为右值。
void transmit(int a, int &&b) {
cout << "b's type is " << type_id_with_cvr<decltype(b)>().pretty_name()
<< endl;
}
int main() {
int a = 2;
int &&b = 3;
// 所以这里不需要一定使用move
// static_cast<decltype(b)>(b)
transmit(a, std::forward<decltype(b)>(b));
return 0;
}
三、使用完美转发
完美转发需要使用到标准库中的std::forward<>()
函数,其定义在头文件<utility>
,其必须通过显式模板实参来调用。
其有两个重载,一个接收左值引用类型参数,另一个接收右值引用类型参数,定义如下:
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::constexpr<Type>
的返回类型是_Tp&&
。需要注意的是这里会触发引用折叠,std::forward<Type>()
可以保持给定实参的左/右引用的属性
修改后transmit()
像下面这样
template <typename F, typename T1, typename T2>
void transmit(F f, T1&& t1, T2&& t2)
{
f(std::forward<T1>(t1), std::forward<T2>(t2));
}
3.1 传递左值分析
我们使用模板修改上面的代码,先以在函数间传递左值为例:
这里 an_other_fun() 函数接受的第二个参数是引用,并且会修改该值:
void an_orther_fun(int a, int& b)
{
std::cout << "in an_orther_fun(): a = " << a << ", b = " << ++b << std::endl;
}
整体代码如下:
void an_other_fun(int a, int &b) {
std::cout << "in an_orther_fun(): a = " << a << ", b = " << ++b << std::endl;
}
template <typename F, typename T1, typename T2>
void transmit(F f, T1 &&t1, T2 &&t2) { // T1=>int&, T2=>int&, 因为`int a`是传值,所以这里不会产生什么影响
f(std::forward<T1>(t1), std::forward<T2>(t2));
}
int main() {
int a = 2, b = 3;
std::cout << " before transmit(): a = " << a << ", b = " << b << std::endl;
transmit(an_other_fun, a, b);
std::cout << " after transmit(): a = " << a << ", b = " << b << std::endl;
return 0;
}
输出如下:
before transmit(): a = 2, b = 3
in an_other_fun(): a = 2, b = 4
after transmit(): a = 2, b = 4
可以看到外层的b确实发生了值修改,接下来对于transmit
函数进行分析:
template <typename F, typename T1, typename T2>
void transmit(F f, T1 &&t1, T2 &&t2) {
// T1=>int&, T2=>int&, 因为`int a`是传值,所以这里不会产生什么影响
f(std::forward<T1>(t1), std::forward<T2>(t2));
}
T2 &&t2
为万能引用,b的表达式类型为左值,所以type(T2)==int&
,根据引用折叠原理type(t2)==int&
。函数std::forward<T2>(t2)
的返回值的类型同样为int&
,表达式类型为左值。
3.2 传递右值分析
...
void an_orther_fun(int a, int&& b) // 注意这里第二个参数是右值引用类型 int&&
{
...
}
...
template <typename F, typename T1, typename T2>
void transmit(F f, T1&& t1, T2&& t2)
{
f(std::forward<T1>(t1), std::forward<T2>(t2));
}
...
transmit(an_orther_fun, a, 3);
...
对于函数void transmit(F f, T1&& t1, T2&& t2)
而言,T2
会被推导为int
类型,从而可以推导出std::forward<T2>(t2)
的返回值为int&&
。