16.2.1 类型转换与模板类型参数
函数模板只有两种情况的类型转换
const转换:可以将一个非const对象的引用(或指针)传递给一个const的引用(或指针)形参- 数组或函数指针转换:如果函数实参不是引用类型,则可以对数组或函数类型的实参应用正常的指针转换
template<typename T> T fobj(T, T);
template<typename T> T fref(const T&, const T&);
string s1("a value");
const string s2("another value");
fobj(s1, s2); // 调用fobj(string, string)
fref(s1, s2); // 调用fref(const string&, const string&)
fobj是按值传递,需要对实参进行拷贝,所以实参是不是const都可以fref是按引用传递,将非const的s1转换为const是允许的
int a[10], b[42];
fobj(a, b); // 调用fobj(int*, int*)
fref(a, b); // 错误,数组类型不同
两个数组大小不同,所以是不同类型
调用fobj时,数组被转换为指针,所以大小不同没关系
调用fref时,因为形参是引用,所以数组不会被转换为指针,大小又不一样,所以调用失败
16.2.2 函数模板显示实参
指定显式模板实参
定义一个允许用户控制返回类型的函数模板
template<typename T1, typename T2, typename T3>
T1 sum(T2, T3);
这样每次调用sum都必须为T1提供一个显式模板实参
long lng = 1024;
int i = 24;
auto val3 = sum<long long>(i, lng);
还要注意模板参数的顺序,如果返回值是T3,就必须为T1,T2,T3都指定实参
对于模板类型参数已经显式指定了的函数实参,也进行正常的类型转换
template<typename T>
int compare(const T &v1, const T &v2);
long lng;
compare(lng, 1024); // 错误,模板参数不匹配 compare(long, int)
compare<long>(lng, 1024); // 正确,compare(long, long)
compare<int>(lng, 1024); // 正确,compare(int, int)
16.2.3 尾置返回类型与类型转换
在模板参数中让用户指定返回类型会给用户增加编程负担,可以使用尾置类型来获取返回类型
比如,我们需要传入容器的迭代器,返回迭代器指向的元素的引用
template<typename It>
auto fcn(It beg, It end) -> decltype(*beg) {
return *beg;
}
fcn的返回类型与解引用beg参数的结果类型相同,解引用运算符返回一个左值,因此通过decltype推断的类型为beg表示的元素的类型的引用
如果要返回迭代器指向元素的拷贝,可以用标准库的remove_reference
template<typename It>
auto fcn2(It beg, It end) -> typename remove_reference<decltype(*beg)>::type {
return *beg;
}
注意:type是一个类的类型成员,所以必须使用typename来告知编译器,type表示一个类型
16.2.4 函数指针和实参推断
可以用函数模板给函数指针赋值
template<typename T> int compare(const T&, const T&);
int (*pf1)(const int&, const int&) = compare;
这里函数指针pf1的参数类型是int,所以用函数模板给它赋值时,编译器会使用pf1的参数类型来推断函数模板实参的类型,所以pf1指向compare的int版本实例
如果不能从函数指针类型推断出模板实参,则报错
// func重载了两个版本
void func(int(*)(const int&, const int&));
void func(int(*)(const string&, const string&));
func(compare); // 错误,不知道调用哪个版本
这里func无法根据形参来推断compare的类型参数,所以只能显式地指定类型参数
func(compare<int>);
当参数是一个函数模板实例的地址时,程序上下文必须满足:对每个模板参数,能唯一确定其类型或值
16.2.5 模板实参推断和引用
从左值引用函数参数推断类型
- 模板参数是普通左值引用时,只能传递左值,可以是
const类型,也可以不是,如果是const,则T被推断为const类型
template<typename T> void f1(T&);
f1(i); // i是一个int,所以T推断为int
f1(ci); // ci是一个const int,所以T推断为const int
f1(5); // 错误,必须传递左值
- 模板参数是
const T&时,可以传递任何实参
template<typename T> void f2(const T&);
f2(i); // i是一个int,所以T推断为int
f2(ci); // ci是一个const int,所以T推断为int,const已经是类型的一部分了
f2(5); // 一个const &参数可以绑定到右值,所以T推断为int
从右值引用函数参数推断类型
- 当模板参数是右值引用,可以传递右值,也可以是左值
template<typename T> void f3(T&&);
f3(42); // 实参是一个int类型的右值,T推断为int
引用折叠和右值引用参数
正常不能把一个右值引用绑定到一个左值,但有两个例外
- 当我们将一个左值传递给函数的右值引用参数,且此右值引用指向模板类型参数时(如
T&&),编译器推断模板类型参数为实参的左值引用类型。因此,当调用f3(i)时,编译器推断T的类型为int& - 如果我们间接创建一个引用的引用,则这些引用形成了折叠
X& &、X& &&和X&& &都折叠成类型X&- 类型
X&& &&折叠成X&&
引用折叠只能应用于间接创建的引用的引用,如类型别名或模板参数
这样,给f3传递左值时,编译器推断T为左值引用,并使用引用折叠
f3(i); // 实参是一个左值,T推断为int&
f3(ci); // 实参是一个左值,T推断为const int&
f3的实例化类似于
void f3<int&>(int& &&); // T是int&
折叠成
void f3<int&>(int&);
这两个规则导致了两个重要结果
- 如果一个函数参数是一个指向模板类型参数的右值引用(
T&&),则它可以被绑定到一个左值 - 如果实参是一个左值,则推断出的模板类型将是一个左值引用,且函数参数将被实例化为一个普通左值引用参数(
T&)
如果一个函数参数值指向模板参数类型的右值引用,则可以传递给它任意类型的实参。如果将一个左值传递给这样的参数,则函数参数被实例化为一个普通的左值引用
16.2.6 理解std::move
template<typename T>
typename remove_reference<T>::type&& move(T&& t) {
return static_cast<typename remove_reference<T>::type&&>(t);
}
string s1("hi"), s2;
s2 = std::move(string("bye")); // 实参是右值
s2 = std::move(s1); // 实参是左值
1、传递右值
- 推断T的类型为
string remove_reference用string进行实例化remove_reference<string>的type成员是stringmove的返回类型是string&&move的函数参数t的类型是string&&
实例化为
string&& move(string&& t) { // 右值
return static_cast<string&&>(t);
}
2、传递左值
T的类型为string&remove_reference用string&进行实例化remove_reference<string&>的type成员是stringmove的返回类型仍是string&&move的函数参数t的类型为string& &&,会折叠为string&
实例化为
string&& move(string& t) { // 左值
return static_cast<string&&>(t); // 转换为右值
}
这里用static_cast显式地将一个左值转换为右值引用
16.2.7 转发
template<typename F, typename T1, typename T2>
void flip1(F func, T1 t1, T2 t2) {
func(t2, t1);
}
void f(int v1, int& v2) {
cout << v1 << " " << ++v2 << endl;
}
如果直接调用f函数
int i = 10;
f(42, i); // 调用结束后i=11,因为是按引用传递
但是在flip1里面调用就不一样了
int j = 10;
flip1(f, j, 42); // 调用结束后j=10,没有改变
因为j传递给t1是按值传递,所以对t1的改动不会影响到j
如果想对j改动,就要按引用传递,所以flip1函数必须能同时支持左值实参和右值实参,要实现这样的功能就需要用右值模板参数
template<typename F, typename T1, typename T2>
void flip2(F func, T1&& t1, T2&& t2) {
func(t2, t1);
}
int j = 10;
flip2(f, j, 42); // 调用结束后j=11
这样,传递左值j时,T推断为int&,经过引用折叠,t1的类型为int&,这样对t1的修改就是对j的修改
传递右值42时,T2推断为int,t2的类型为int&&
如果一个函数参数是指向模板类型参数的右值引用,它对应的实参的
const属性和左值/右值属性将得到保持
假设现在不调用f,改成调用g,把第一个参数改成右值引用
template<typename F, typename T1, typename T2>
void flip2(F func, T1&& t1, T2&& t2) {
func(t2, t1);
}
void g(int&& v1, int& v2) { // 第一个参数是右值引用
cout << v1 << " " << v2 << endl;
}
flip2(g, j, 42); // 错误,不能将左值实参的t2绑定到右值形参v1
这里传递给t2是一个右值42,所以t2的类型为int&&,但是t2是变量,是左值,传递给g时,因为v1是右值引用,不能绑定到一个左值,所以报错
在调用中使用std::forward保持类型信息
// 转发左值
template<typename T>
T&& forward(typename std::remove_reference<T>::type& t)
{
return static_cast<T&&>(t);
}
// 转发右值
template<typename T>
T&& forward(typename std::remove_reference<T>::type&& t)
{
return static_cast<T&&>(t);
}
我们希望保持42这个右值的类型信息,不要让它变成左值,可以使用std::forward,forward必须通过显式模板参数来调用,返回该实参类型的右值引用,即,forward<T>的返回类型是T&&
template<typename F, typename T1, typename T2>
void flip2(F func, T1&& t1, T2&& t2) {
func(std::forward<T2>(t2), std::forward<T1>(t1));
}
flip2(g, j, 42); // 正确
这样,T2推断为int,forward返回类型是int&&,可以传递给函数g中的右值引用参数v1;而T1推断为int&,forward的返回类型为int& &&,折叠后是int&,可以传递给函数g中的左值引用参数v2