完美转发指的是函数模板可以将自己的参数“完美”地转发给内部调用的其它函数。所谓完美,即不仅能准确地转发参数的值,还能保证被转发参数的左、右值属性不变。
文章目录
- 场景
- 旧的方法
- 新的方法
- 内部实现
- 参考文献
场景
思考下面的代码:
template<typename T>
void function(T t) {
otherfunc(t);
}
function()
函数模板中调用了otherfunc()
函数。我们想要的完美转发是:
- 如果
function()
函数接收到的参数t
为左值,那么该函数传递给otherfunc()
的参数t
也是左值。 - 如果
function()
函数接收到的参数t
为右值,那么传递给otherfunc()
函数的参数t
也是右值
显然 function()
函数模板没有实现完美转发,这是因为无论是左值还是右值传递进来,都会当作是左值,因为是非引用类型。
比如 function(10);
传递给 otherfunc()
也是左值而不是我们期望的右值,这在我们期望对左值和右值进行不同处理时会产生问题。
旧的方法
在C++98/03 标准下的 C++ 也可以实现完美转发,只是实现方式比较麻烦。
C++98/03 标准中只有左值引用,可以细分为非 const引用和const引用:
- 非const引用作为函数模板参数,只能接收左值无法接收右值
- const左值引用既可以接收左值,也可以接收右值,但如果内部需要将参数传递给其他函数,需要被调用函数的参数也是 const,否则无法直接传递。
可见能实现转发,但不够"完美"。
#include <iostream>
using namespace std;
//重载被调用函数,查看完美转发的效果
void otherfunc(int & t) {
cout << "call lvalue\n";
}
void otherfunc(const int & t) {
cout << "call rvalue\n";
}
//重载函数模板,分别接收左值和右值
//接收右值参数
template <typename T>
void function(const T& t) {
otherfunc(t);
}
//接收左值参数
template <typename T>
void function(T& t) {
otherfunc(t);
}
int main()
{
function(10);//10 是右值
int x = 2;
function(x);//x 是左值
return 0;
}
输出为
左值实参既能匹配 function(T& t)
也能匹配 function(const& t)
,编译器会选择更合适的 function(T& t)
。
新的方法
对于旧的方法,当模板函数有大量参数的情况,可能就需要编写大量的重载函数模板。
在C++11标准中引入了右值引用,通常情况下右值引用只能接收右值,而对于函数模板中使用右值引用语法定义的参数来说,它既可以接收右值,也可以接收左值(称为万能引用)。
因此在C++11标准中实现完美转发,只需要编写如下一个模板函数即可:
template <typename T>
void function(T&& t) {
otherdef(t);
}
但是还存在一个问题,如果我们传入的参数是一个左值引用或右值引用的实参,如下所示:
int x = 5;
int& y = x;
function(y); // T为int&
int&& z = 1;
function(z); // T 为int&&
其中, function(y)
实例化的函数为 function(int& && t)
,由function(z)
实例化的函数为 function(int&& &&t)
,这在C++98/03是不支持的,而C++11引入了引用折叠规则:
- 当实参为左值或者左值引用(
A&
)时,函数模板中T&&
将转变为A&
,即A& &&
=A&
。 - 当实参为右值或者右值引用(
A&&
)时,函数模板中T&&
将转变为A&&
,即A&& &&
=A&&
。
还存在的问题是,在function()
函数内部,不管是 T& t
还是 T&& t
其实 t
都是一个左值,因此都会传递到 otherfunc(int& t)
。
所以我们需要一种解决方案来处理这个问题,C++11标准里的模板函数 forward()
就是用来解决这个问题,让我们能传递左值/右值属性,例子如下:
#include <iostream>
using namespace std;
//重载被调用函数,查看完美转发的效果
void otherfunc(int & t) {
cout << "lvalue\n";
}
void otherfunc(const int & t) {
cout << "rvalue\n";
}
//实现完美转发的函数模板
template <typename T>
void function(T&& t) {
otherfunc(forward<T>(t));
}
int main()
{
function(1);
int x = 2;
function(x);
return 0;
}
输出如下,正确传递了左值/右值属性
内部实现
下面简单看下内部实现(MSVC)
_EXPORT_STD template <class _Ty>
_NODISCARD _MSVC_INTRINSIC constexpr _Ty&& forward(remove_reference_t<_Ty>& _Arg) noexcept {
return static_cast<_Ty&&>(_Arg);
}
_EXPORT_STD template <class _Ty>
_NODISCARD _MSVC_INTRINSIC constexpr _Ty&& forward(remove_reference_t<_Ty>&& _Arg) noexcept {
static_assert(!is_lvalue_reference_v<_Ty>, "bad forward call");
return static_cast<_Ty&&>(_Arg);
}
简单地来说就是通过静态的强制类型转换+引用折叠,返回对应的结果。
比如下面的四钟情况:
int x = 2;
otherfunc(forward<int>(x)); // 匹配第一个,返回 int&&
otherfunc(forward<int>(2)); // 匹配第二个,返回 int&&
int& y = x;
otherfunc(forward<int&>(y)); // 匹配第一个,返回 int&
int&& z = 2;
otherfunc(forward<int&&>(z)); // 匹配第一个,返回 int&&,可见右值引用是个左值
它的输出结果如下
参考文献
C++11、C++14、C++17、C++20新特性总结(5万字详解)