模板实参推断和引用
从左值引用函数参数推断类型
template <typename T>
void f1(T& param)
{
}
int i = 10;
const int ci = 3;
f1(i); // i 是一个int,模板参数T是int
f1(ci); // ci 是一个const int,但模板参数T是const int
f1(5); // 错误,传递给一个&参数的实参必须是一个左值
如果这里函数参数的类型是const T&
,正常的绑定规则告诉我们可以给它传递任何类型的实参:
- 一个对象(const或非const)
- 一个字面值。
从右值引用函数参数推断类型
当一个函数参数是一个右值引用(即形如T&&)时,正常绑定规则告诉我们可以传递给它一个右值。
template <typename T>
void f2(T&& param)
{
}
f2(5);
f2(std::move(i));
f2(std::move(ci));
-
右值引用参数的引用折叠
在上面的调用中,假设我们用f2(i)
这样看起来时不合法的,因为
i
是一个左值,而我们是不能将一个右值引用绑定到左值上。但是实际上是可以的,因为C++这里有例外规则:当我们将一个左值传递给一个传递给函数的右值引用参数,且此右值引用指向模板类型参数时(
T&&
)时,编译器会推断模板类型 类型参数为实参的左值引用类型(T&
)。template <typename T> void f3(T&& param) { } f3(i); // 这里T的类型是int & f3(ci); // 这里T的类型是const int &
所以这里T为
int&
,int& &&
看起来好像意味着是一个int&
的右值引用,但是通常我们不能定义一个引用的引用。在这种情况下:
1、X& &, X& && 和 X&& & 都折叠成类型X&
2、类型X&& && 折叠成X&&
这里的param类型就为int&
。
上面的规则意味着:我们可以将任意类型的实参传递给T&&
类型的函数参数。
template <typename T>
void f3(T&& param)
{
}
f3(3); // T 是int
f3(i); // T 是int&
f3(ci); // T 是const int&
std::move
template <typename T>
typename remove_reference<T>::type&& move(T&& t)
{
return static_cast<remove_reference<T>::type&&>(t);
}
首先规定:
static_cast
可以显示的将一个左值转换为一个右值引用-
首先,move的函数参数是
T&&
,通过引用折叠,此参数可与任何类型的实参匹配。std::string s1("hi"); s2 = std::move(std::string("bye")); s2 = std::move(s1);
以
std::move(std::string("bye"))
为例:- 推断出 T 的类型为string
- remove_reference<string>的type成员是 string
- static_cast<remove_reference<T>::type&&>(t) 为 static_cast<string&&>(t)
以
std::move(s1)
为例:- 推断出 T 的类型为string &(引用折叠)
- remove_reference<string&>的type成员是 string
- static_cast<remove_reference<T>::type&&>(t) 为 static_cast<string&&>(t)
std::forward
某些函数需要将一个或多个实参连同类型不变地转发给其他函数。在此情况下,我们需要保持被转发实参的所有性质:
- 包括实参类型是否是
const
- 实参是左值还是右值
template <typename F, typename T1, typename T2>
void flip1(F f, T1 t1, T2 t2)
{
f(t1, t2);
}
void f(int t1, int& t2)
{
cout << t1 << " " << ++t2 << endl; // v2是引用,这里需要改变v2的值
}
int j = 20;
flip1(f, 42, j); // 42 21
cout << j << endl; // 20
这里问题在于:这个flip1(f, 42, j)
调用推断为:T1 为 int,T2 为int,F为void (*fcn)(int t1, int& t2)
在这一步中j
的值被拷贝到t2
中,f
中的引用参数被绑定到t2
,而非j
,从而其改变不会影响j
。
所以我们能想到的是:我们需要重写函数,使其参数能够保持给定实参的"左值性",进一步,也希望能保持参数的const属性。
template <typename F, typename T1, typename T2>
void flip2(F f, T1 &&t1, T2 &&t2)
{
f(t1, t2);
}
void f(int t1, int &t2)
{
cout << t1 << " " << ++t2 << endl; // v2是引用,这里需要改变v2的值
}
int j = 20;
flip1(f, 42, j);
cout << j << endl;
这里flip2(f, 42, j)
的调用推断为:T1为int,t1类型为int&&,T2为int&,t2 的类型会折叠为int&,F为void (*fcn)(int t1, int& t2)
。
可以看出来,这个版本的 flip2 解决了一些问题,但现在:
template <typename F, typename T1, typename T2>
void flip2(F g, T1 &&t1, T2 &&t2)
{
g(t1, t2);
}
void g(int &&i, int &j)
{
cout << i << " " << j << endl;
}
int i = 20;
flip2(g,42, i); // 错误,
这里flip2(g, 42, i)
的调用推断为:T1为int,t1的类型为int&&,T2为int&,t2 类型折叠后为int&,F为void (*fcn)(int &&i, int &j)
。但是函数参数与任何变量一样,都是左值,所以这里要报错。
这时,我们可以用std::forward
来传递参数,它能保持原始实参的类型。
template <typename F, typename T1, typename T2>
void flip2(F g, T1 &&t1, T2 &&t2)
{
g(std::forward<T1>(t1), std::forward<T2>t2);
}
forward 实现原理
template <typename T>
T&& forward(typename std::remove_reference<T>::type& param)
{
return static_cast<T&&>(param);
}
template <typename T>
T&& forward(typename std::remove_reference<T>::type&& param)
{
return static_cast<T&&>(param);
}
理解std::move之后就差不多能理解std::forward。
对比
template <typename T>
void print(T &t)
{
std::cout << "左值" << std::endl;
}
template <typename T>
void print(T &&t)
{
std::cout << "右值" << std::endl;
}
template <typename T>
void testForward(T &&v)
{
print(v);
print(std::forward<T>(v));
print(std::move(v));
}
int main()
{
testForward(1);
std::cout << "======================" << std::endl;
int x = 1;
testForward(x);
}
变量是左值,所以第一个prin(v)会调用第一个函数
std::move 会始终调用第二个函数。
对于1,推断T为int
,v为int&&
,保持原属性,右值。
对于x,推断T为int&
,引用折叠v为int&
,保持原属性,左值。
左值
右值
右值
======================
左值
左值
右值
参考资料
1、《C++ primer》