std::forward 个人理解
模板函数里面的std::forward<T>,"T"代表什么意思
template<typename T>
Print(T&& param)
{
// 打印 t 的类型
}
template<typename T>
foo(T&& param)
{
Print(std::forward<T>(param));
}
int main()
{
int a = 10; // a 是左值
int& lrefa = a; // lrefa 是左值引用
int&& rrefa = 10; // rrefa 是右值引用
foo(a);
foo(lrefa);
foo(std::forward<decltype(rrefa)>(rrefa));
// rrefa是右值引用类型的左值,所以要将右值的类型传入进foo,这样param才是右值
// 如果是foo(rrefa),就相当于传入了一个左值
}
/*
Output:
传入之前的类型:
a type: int
lrefa type: int & __ptr64
rrefa type: int && __ptr64
传入左值:
in foo, T type: int & __ptr64
in foo, param type: int & __ptr64
in Print, T type: int & __ptr64
in Print, param type: int & __ptr64
传入左值引用:
in foo, T type: int & __ptr64
in foo, param type: int & __ptr64
in Print, T type: int & __ptr64
in Print, param type: int & __ptr64
传入右值引用:
in foo, T type: int
in foo, param type: int && __ptr64
in Print, T type: int
in Print, param type: int && __ptr64
*/
推导过程
首先说明一下推导规则
规则一:
template<typename T>
f(T& param) // 形参类型是一个引用,但不是万能引用
f(expr) // 调用,实参是expr
- 如果expr类型是一个引用,忽略引用部分
- expr有const,ParamType不是const,T推导保留const性
规则二:
template<typename T>
f(T&& param) // 形参类型是万能引用
- 如果expr是左值,T和param都被推导为左值引用
- 如果expr是右值,T就是右值类型,param是右值引用
规则三:
template<typename T>
f(T param) // 形参类型是传值的方式,param就是一个拷贝
- 如果expr类型是一个引用,忽略引用部分
- 忽略引用性后,expr的const,volatile也忽略
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);
}
实参转移过程
main(): a → foo(a) → std::forward(a) → Print(a)
其中在 foo(T&& param) 和 Print(T&& param) 这里会发生类型推导,在foo中推导的T会影响std::forward(a)中的T的类型,std::forward(a)转发得到的类型影响Print(T&& t)中T的推导
决定链为:
- a type → foo(T&& param)
- foo(T&& param) T type → std::forward(a) T type // 这里foo推导出来的T type和forward转发结果是一致的
- std::forward(a) return type → Print(T&& param) T type
这样一串调用下来,就能够将在main函数中a的类型,完美转发到了Print()函数中
f(a)推导
- main(): a 传入 foo 之前,a 的类型是 int
- ⇒ foo(T&& param): a 传入 foo 后,a 是左值,经过类型推导,规则二,左值推导为左值引用,所以T被推导为int&,int& && param 发生引用折叠后,param为int&
- ⇒ std::forward(param): 传入的param是左值,调用左值转发,此时T的类型为int&故转发的类型就是int&
- ⇒ Print(T&& t): 从上一个函数里传入的实参类型为int&,传入的是左值,故为T推导为左值引用int&,param引用折叠推导为int&
main | foo(T&& param) | forward(T& param) | Print(T&& param) |
---|---|---|---|
param: int | T: int& | T: int& | T: int& |
param: int& && = int& | param: int& | param: int& && = int& | |
return: int& && = int& |
f(lrefa)推导
- main(): lrefa传入foo之前,lrefa 的类型是 int&
- ⇒ foo(T&& param): 实参a的类型是int&,a是左值,故T推导为左值引用 int&,param经过引用折叠int& && 推导为 int&
- ⇒ std::forward(param): 传入的param是左值,调用左值转发,此时T的类型为int& 转发后仍为int&
- ⇒ Print(T&& t): 从上一个函数里传入的实参类型为int&,传入的是左值,故T推导为左值引用int&,param引用折叠推导为int&
main | foo(T&& param) | forward(T& param) | Print(T&& param) |
---|---|---|---|
param: int& | T: int& | T: int& | T: int& |
param: int& && = int& | param: int& & = int& | param: int& && = int& | |
return: int& && = int& |
f(rrefa)推导
- main(): rrefa传入foo之前,rrefa 的类型是 int&&
- ⇒ foo(T&& param): 实参类型是 int&&,根据推导规则二,T被推导为右值类型,即 int,param被推导为 int&& && ,引用折叠后param类型为int&&
- ⇒ std::forward(param): 此时T的类型为int,故std::forward(param),给forward传入的实参类别是一个左值,调用了forward的转发左值函数,传给forward的模板类型是int(这里param是一个拥有右值引用类型的左值变量),故forward返回的结果是int&&
- ⇒ Print(T&& t): 经过forward转发,传入的类型是int&&,故T的类型推导为int,t的类型推导为int&&
main | foo(T&& param) | forward(T& param) | Print(T&& param) |
---|---|---|---|
param: int&& | T: int | T: int | T: int |
param: int&& | param: int& | param: int&& | |
return: int&& |
理解 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);
}
从两个函数定义来看,remove_reference<_Tp>::type& __t 的参数就是一个左值引用,remove_reference<_Tp>::type&& __t 的参数就是一个右值引用,他们都不需要类型推导(因为无论_Tp推导为什么类型,都被移除引用性,只保留了基本的类型,如int,string…)。由于forward用法是forward这样就说明了具体使用哪一种转发
如果传入的是一个左值,就调用转发左值函数,如果传入的是右值,就调用转发右值函数。这里的左值和右值完全不看变量自身的类型,而是看其类别。左值和右值在生命周期方面的不同
传入参数类型 | 调用转发函数 | forward<decltype(param)>(param) |
---|---|---|
param: int右值 | 调用右值转发 | T: int param: int&& return: int&& |
param: int&&左值 | 调用左值转发 | T: int&& param: int& return: int&& && → int&& |
param: int左值 | 调用左值转发 | T: int param: int& return: int&& |
param: int&左值 | 调用左值转发 | T: int& param: int& return: int& && → int& |
※任何函数的内部,对形参的直接使用,都是按左值进行的
如果要将一个右值引用类型的左值,以右值的属性传入给函数,那么就必须使用forward,将其类型的右值属性一起转发过去
不允许的情况:forward<int&>(100) 这种用法是不允许的,这种写法的意思是,将一个右值,转发成为一个左值,而右值是不能转成左值的,故触发了static_assert,禁止这种写法
转发的返回结果类型是由强制转换+引用折叠共同决定的,如forward<int>这种就是转发右值,返回结果是int&&,forward<int&>这种是转发左值引用,返回结果是int& &&→int&
模板函数的参数是万能引用,如果在函数体内传递参数,直接使用形参就会按照以左值的形式传参,使用forward就可以实现,按照原来的类型进行传参
总结
实际上,”T”的声明,正好显式表现了通过转发,将得到结果的类型,forward 转发的就是 forward<T>里面T的类型,如果T为左值引用,则转发的结果为左值引用,如果T为右值/右值引用,则转发的结果为右值引用
在使用时,如果在参数是万能引用的模板函数里,可以使用forward,如果不是万能引用的话,使用forward<decltype(param)>来转发其自身类型
forward就是为了在任何函数体内进行传参时,保持变量其自身的类型,而不是直接使用形参以左值的方式处理
💡 例:
右值转发:forward(param) —> int&& 或 forward<int&&>(param) —> int&&左值转发:forward<int&>(param) —> int&
如果要转发其自身的类型,使用:
forward<decltype(param)>(param)