# 模板中的实参推断和引用
# 从左值引用函数参数推断类型
- 编译器会应用正常的引用绑定规则
- const是底层的不是顶层的
template<typename T>
void f1(T &t)
{
std::cout << t << std::endl;
}
void main(void)
{
int a = 100;
f1(a); // T = int
const int ci = 100;
f1(ci); // T = const int
f1(10); // error
}
在上面代码中,模板函数f1
参数是一个左值引用T&
, 因此只能接受一个左值,可以是const
类型,但不能是一个右值。
若模板函数参数是常引用const T&
,则函数参数可以传入给它任何类型的实参,一个对象const
或非const
、临时对象、字面常量。
template<typename T>
void f1(const T &t)
{
std::cout << t << std::endl;
}
void main(void)
{
int a = 100;
f1(a); // T = int
const int ci = 100;
f1(ci); // T = const int
f1(10); // OK
}
# 从右值引用函数参数推断类型
template <typename T>
void f2(T &&t)
{ }
void main(void) {f2(10); }// T = int
如上的右值引用参数函数,当给其传入右值实参进行函数实例化时,会按右值引用传入。
但对于上面的函数f2
,会惊奇的发现竟然能够传入左值,
void main(void) {
int a = 10;
f2(a); // T = int&
}
如果对于普通的非模板函数f2(a)
会立刻提示语法错误,但对于模板函数,定义了两个例外的编译规则,
第一个编译规则,当将一个左值传递给模板函数的模板类型右值引用的参数时,编译器推断模板类型参数为实参的左值引用类型。所以调用f2(a)
时,T
被推断成了int&
而非int
。
T
被推断成int&
,f2
的参数好像被推断成了int& &&
,貌似是一个int&
类型的右值引用。在C++
中不能直接定义引用的引用,但是就像这样,有可能间接产生引用的引用。为了处理间接定义的引用的引用,C++
中又定义了第二个例外的编译规则,当间接定义了引用的引用时,这些引用会形成折叠。T& &/T& &&/T&& &
都会被折叠成T&
。T&& &&
被折叠成T&&
。
上面的例子,当定义接受右值引用参数的模板函数时,因为即能传入左值实例化成T&
,又能传入右值,会导致意向不到的效果。
template<typename T>
T fcn(T t)
{
return T + 1
}
template <typename T>
void f2(T &&t)
{
T val = t; // 这里是把值t赋值给val, 还是左值的引用别名呢?
val = fcn(val); // 赋值给`val`会不会改变t的值呢?
}
void main(void)
{
int a = 10;
f2(a); // 这时是`T=int&`
f2(10); // `T=int
}
如此就会导致函数的歧义,编写模板函数时处理起来就很麻烦。实际中,右值引用的使用情况有两种,一个是模板转发其实参,就是将参数原封不动的传给其调用的函数;另一种是模板被重载。
解决这个问题的一个方法是利用函数重载,写两个模板函数,如:
// a 可变右值引用调用
template<typename T>
void f1(T &&t)
{
int a = 100;
t = 100;
std::cout << t << std::endl;
}
// b 左值引用或const右值引用调用
template<typename T>
void f1(const T&t)
{
int a = 100;
std::cout << t << std::endl;
}
void main(void)
{
int a = 100;
const int b = std::move(a);
f1(10); // 可变右值引用调用a
f1(b); // const右值引用调用b
return 0;
}
# std::move--右值引用模板函数的一个实例
std::move
函数用来将左值转换成右值。其在标准库中的定义,
template<typename _Tp>
constexpr typename std::remove_reference<_Tp>::type&&
move(_Tp&& __t) noexcept
{
return static_cast<typename std::remove_reference<_Tp>::type&&>(__t);
}
从上面的代码可以看到,move
函数的参数是指向模板类型_Tp
右值引用。在上一部分的介绍,这种根据引用折叠的规则也可以接受左值。
string s1("cpp"), s2;
s2 = std::move(s1); // case 1, 传入左值,`s1`按引用传入, 导致s1的值不确定
s2 = std::move(string("c")); // case2, 传入右值,从有值移动数据。
case1
中是按左值引用传入的s1
,case2
中,向一个右值引用函数参数传入一个右值时,右实参推断出的类型是被引用的类型。
在上面调用std::move(string("c"))
时,
- 根据实参推断的T的类型为
string
remove_reference
用string
来实例化- 模板结构体
remove_reference
的成员type
为string
move
的返回类型是string&&
move
的函数参数__t
的类型为string&&
此时move
实例后的函数为:
string &&move(string &&)
当传入的参数是左值时,
- 根据实参推断出的
_Tp
的类型为string&
remove_reference
用string&
来实例化remove_reference<string&>
的type
成员是string
move
的返回类型仍然是string&&
move
参数的类型经由引用折叠变成了string&
此时move
实例后的函数为:
string &&move(string &)
remove_reference
是一个模板类,并有左值和右值引用的实例模板,可以取出左值右值引用的原始类型。static_cast
可以从一个左值转换成右值引用。
# 参数转发
有时候在编写函数模板时会希望,模板函数能将传入的参数原封不动的转发给模板函数被调用的函数,譬如保持参数引用,const
,左值还是右值等属性。
看一个模板函数,其接受一个可调用表达式和两个额外实参,
void fun1(int a, int &b)
{
std::cout << a << std::endl << ++b << std::endl;
}
template<typename F, typename T1, typename T2>
void flip1(F f, T1 t1, T2 t2)
{
fun1(t2, t1);
}
void main(void)
{
int t1 = 10;
flip1(fun1, t1, 10);
std::cout << "t1: " << t1 << std::endl; // t1: 10
}
调用函数后会发现t1
的值没有能够改变,这是因为模板函数被实例成了
void flip1(void(*fun1)(int, int&), int, int)
可以看到变量在传入函数flip1
中时,是按值传递的,而不是引用,因此调用函数flip1
传入的边值的值并不会被改变。
有人可能会想到前面的右值引用的模板函数可以接受左值,而且被折叠成了左值引用,不是刚好可以用吗?
void fun1(int a, int &b)
{
std::cout << a << std::endl << ++b << std::endl;
}
template<typename F, typename T1, typename T2>
void flip1(F f, T1 &&t1, T2 &&t2)
{
fun1(t2, t1);
}
void main(void)
{
int t1 = 10;
flip1(fun1, t1, 10);
std::cout << "t1: " << t1 << std::endl; // t1: 11
}
通过上面的代码可以发现,确实t1
是按左值引用传入的,t1
的值被变成了11
。
但是,对于接受右值引用参数的函数效果怎么样呢? 很遗憾,上面的方法依然存在问题。例如将fun1
改成接受右值引用参数的函数,
void fun1(int &&a, int &b)
{
std::cout << a << std::endl << ++b << std::endl;
}
template<typename F, typename T1, typename T2>
void flip1(F f, T1 &&t1, T2 &&t2)
{
/**
* 编译报错,传入10,t2是右值引用,t2是一个变量是一个左值,
* 使用左值调用接受右值引用的函数,编译会报错
*/
fun1(t2, t1);
}
void main(void)
{
int t1 = 10;
flip1(fun1, t1, 10);
std::cout << "t1: " << t1 << std::endl;
}
上面的代码将无法通过编译,因为flip1
的参数t2
是左值,类型是int&&
,无法直接传递给fun1
函数。
在标准库中有std::forward
模板函数,专门用来做函数的参数转发,可以保持原始实参的类型。通过其返回类型上的引用折叠,forward
可以保持给定实参的左值右值属性。
通过在模板函数中使用std::forward
函数,就可以实现上面的代码了,
void fun1(int &&a, int &b)
{
std::cout << a << std::endl << ++b << std::endl;
}
template<typename F, typename T1, typename T2>
void flip1(F f, T1 &&t1, T2 &&t2)
{
fun1(std::forward<T2>(t2), std::forward<T1>(t1));
}
void main(void)
{
int t1 = 10;
flip1(fun1, t1, 10);
std::cout << "t1: " << t1 << std::endl;
}