从函数实参类确定模板实参的过程称为模板实参推断(template argument deduction)。
类型转换和模板类型参数
如果一个函数形参的类型使用了模板类型参数,那么它采用特殊的初始化规则。只有很有限的几种类型转换会自动应用于这些实参。编译器通常不是对实参进行类型转换,而是生成一个新的模板实例。能在调用中应用于函数模板的包括如下两项:
- 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);const被忽略
fref(s1, s2); //调用fref(cosnt string&, const string&)
//将s1转换成const是允许的
int a[10], b[42];
fobj(a, b); //调用f(int*, int*)
fref(a, b); //错误:数组类型不匹配
使用相同模板参数类型的函数参数
一个模板类型参数可以用作多个函数形参的类型。由于只允许有限的几种类型转换,因此传递给这些形参的实参必须具有相同的类型。
long lng;
compare( lng, 1024); //错误:不能实例化compare(long, int)
如果希望允许对函数实参进行正常的类型转换,我们可以将函数模板定义为两个类型参数:
template <typename A, typename B>
int filexibleCompare( const A& v1, const B& v2)
{
//……
}
正常类型转换应用于普通函数实参
函数模板可以有用普通类型定义的参数,即,不涉及模板类型参数的类型。这种函数实参不进行特殊处理,它们正常转换为对应形参的类型:
template <typename T> ostream &print( ostream &os, const T &obj)
{
return os << obj;
}
print(cout, 43); //实例化print(ostream&, int)
ofstream f("output");
print(f, 10); //使用print(ostream&, int):将f转换成ostream&
函数模板显式实参
指定显式模板实参
template <typename T1, typename T2, typename T3>
T1 sum(T2, T3);
//T1显式指定,T2和T3是从函数实参类型推断而来的
auto val3 = sum<long long>(i, lng); //long long sum(int,long)
显式模板实参按由左至右的顺序与对应的模板参数匹配:第一个模板参数与第一个模板参数匹配,第二个实参与第二个参数匹配,依次类推。只有尾部(最右)参数的显示模板实参才可以忽略:
//糟糕的设计:用于必须指定所有三个模板参数
template <typename T1, typename T2, typename T3>
T3 alternative_sum(T2, T1);
//错误:不能推断前几个模板参数
auto val3 = alternative_sum<long long>(i, lng);
正常类型转换应用于显示指定的实参
对于用普通类型定义的函数参数,允许进行正常的类型转换:
long lng;
compare<long>(lng, 1024);
compare<int>(lng, 1024);
尾置返回类型与类型转换
当我们希望用户确定返回类型时,用显式模板实参表示模板函数的返回类型是很有效的。但在其他情况下,要求显式指定模板实参会给用户增添额外的负担且不会有什么好处。
template <typename T>
??? &fcn(T beg, T end)
{
//……
return *beg;
}
vector<int> vi = {1, 2, 3, 4, 5};
Blob<string> ca = {"hi", "bye"};
auto &i = fcn(vi.begin(), bi.end());
auto &s = fcn(ca.begin(), ca.end());
在此例中,我们知道函数应该返回*beg,而且知道我们可以用decltype(*beg)来获取此表达式的类型。但是在编译器遇到函数的参数列表之前,beg都不存在。为了定义此函数,我们必须采用位置返回类型。
template <typename T>
auto fcn(T beg, T end)->decltype(*beg)
{
//……
return *beg;
}
进行类型转换的标准库模板类
有时我们无法直接获取所需的类型。比如希望编写一个返回元素值而不是引用的类似fcn的函数。对于传递的参数的类型,我们几乎一无所知,唯一可以使用的操作符是迭代器操作,而所有迭代器操作都不会生成元素,只能生成元素的引用。为了获取元素类型,我们可以使用标准库的类型转换(type transformation)模板。这些模板定义在头文件中type_traits。这个头文件中的类通常用于所谓的模板元程序设计。
#include <type_traits>
remove_reference<decltype(*beg)>::type
remove_reference::type脱去引用,剩下元素类型本身。我们必须在返回类型的什么中使用typename来告知
templare <typename T>
auto fcn2(T beg, T end)->typename remove_reference<decltype(*beg)>::type
{
return *beg;
}
对Mod<T>,其中Mod为 | 若T为 | 则Mod<T>::type为 |
remove_reference | X&或X&& 否则 |
X T |
add_const | X&、const X或函数 否则 |
T const T |
add_lvalue_reference | X& X&& 否则 |
T X& T& |
add_rvalue_reference | X&或X&& 否则 |
T T&& |
remove_pointer | X* 否则 |
X T |
add_pinter | X&或X&& 否则 |
X* T* |
make_signed | unsigned X 否则 |
X T |
make_unsigned | 带符号类型 否则 |
unsigned X T |
remove_extent | X[n] 否则 |
X T |
remove_all_extents | X[n1][n2]… 否则 |
X T |
函数指针和实参推断
template <typename T> int compare(const T&, const T&);
//pf1指向实例int compare(const int&, const int&)
int (*pf1)(const int&, const int&) = compare;
//func的重载版本:每个版本接受一个不同的函数指针类型
void func(int(*)(const string&, const string&));
void func(int(*)(const int&, const int&));
//错误:二义性
func(compare);
//正确:显式指出实例化版本
fun(compare<int>);
当参数是一个函数模板实例的地址时,程序上下文必须满足:对每个模板参数,能唯一确定其类型或值
模板实参推断和引用
template <typename T> void f(T &p);
函数参数p是一个模板类型参数T的引用,非常重要的是记住两点:编译器会应用正常的引用绑定规则;const是底层的,不是顶层的。
从左值引用函数参数推断类型
当一个函数参数是模板类型参数的一个普通(左值)引用时(T&),只能传递给它一个左值。实参如果是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&,正常的绑定规则告诉我们可以传递给它任何类型的实参——一个对象(const或非const)、一个临时对象或是一个字面值常量。
template <typename T> void f2(const T&);
f2(i); //i:int; T:int
f2(ci); //ci:const int; T: int
f2(5); //正确
引用折叠和右值引用参数
template <typename T> void f3(T&&);
通常不能将一个右值引用绑定到一个左值上。但C++语言在正常绑定规则之外定义了两个例外规则,允许这种绑定。这两个例外规则是move这种标准库设施正确工作的基础:
- 第一个例外规则影响右值引用参数的推断如何进行。当将一个左值传递给函数的右值引用参数,且此右值引用指向模板类型参数(T&&),编译器推断模板类型参数为实参的左值引用类型。通常,不能直接定义一个引用的引用,但通过类型别名或通过模板类型参数间接定义是可以的。
- 如果间接创建了一个引用的引用,则这些引用形成了“折叠”。在所有情况下(除了一个例外),引用会折叠成一个普通的左值引用类型。只在一种特殊情况下引用会折叠成右值引用:右值引用的右值引用。即,给定一个类型X:
- X& &,X& &&和X&& &都折叠成类型X&
- X&& &&折叠成X&&
引用折叠值能应用于间接创建的引用的引用,如类型别名或模板参数。
f3(i); //实参是一个左值;模板参数T是int&
f3(ci); //实参是一个左值;模板参数T是const int&
//用于演示的无效代码
void f3<int &>(int& &&); //T是int&,函数参数是int& &&。折叠为int&
如果一个函数参数是指向模板参数类型的右值引用(T&&),则可以传递给它任一类型(左值或右值)的实参。如果将一个左值传递给这样的参数,则函数参数被实例化为一个普通的左值引用。
template <typename T> void f3(T&& val)
{
T t = val; //拷贝 OR 绑定一个引用
t = fcn(t); //赋值只改变t OR 又改变t又改变val
if(val == t) //若T为引用类型,一直为true
{
/* */
}
}
当我们f3(42)时,T为int,局部变量t是int,通过拷贝参数val的值初始化。对t赋值,参数val保持不变
当我们f3(i)时,T为int&,定义并初始化局部变量t时,类型为int&。
在实际中,右值引用通常用于两种情况:模板转发其实参或模板被重载。重载通常如下方式:
template <typename T> void f(T&&); //绑定到非const右值
template <typename T> void f(const T&); //左值和const右值
理解std::move
标准库如下定义move
template <typename T>
typename remove_reference<T>::type&& move(T&& t)
{
return static_cast<typename remove_reference<T>::type&&>(t);
}
move的函数参数T&&是一个指向模板类型参数的右值引用。通过引用折叠,此参数可以与任何类型的实参匹配。
string s1("hi"), s2;
s2 = std::move(string("bye")); //正确,从一个右值移动数据
s2 = std::move(s1); //正常:但在赋值后,s1的值是不确定的
在std::move(string("bye"))中:
- 推断出T的类型是string
- 因此,remove_reference用string实例化
- remove_reference<string>::type是string
- move返回类型是static_cast<string&&>(t),即string&&
- move的函数参数t的类型是string&&
- 所以这个调用实例化move<string>,即函数 string&& move(string &&t)
在std::move(s1)中:
- T的类型是string的引用
- 因此,remove_reference用string&实例化
- remove_reference<string&>::type是string
- move返回类型是static_cast<string&&>(t),即string&&
- move的函数参数t实例化为string& &&,这得为string&
- 所以这个调用实例化move<string&>,即函数string&& move(string &t)
从一个左值static_cast到一个右值引用是允许的。通常情况,static_cast只能用于其他合法的类型转换。但是,这里又有一条针对右值引用的特许规则:虽然不能隐式的将一个左值转换成右值引用,但可以用static_cast显式的将一个左值转换成一个右值引用。
转发
某些函数需要将其一个或多个实参连同类型不变的转发给其他函数。在此情况下,我们需要保持被转发实参的所有性质,包括实参类型是否是const以及实参是左值还是右值:
//接受一个可调用对象和另外两个参数的模板
//对“翻转”的参数调用给定的可调用对象
//flip1是一个不完整的实现:顶层const和引用丢失了
template<typename F, typename T1, typename T2>
void flip1(F f, T1 t1, T2 t2)
{
f(t2, t1);
}
void f(int v1, int &v2)
{
cout << v1 << " " << ++v2 << endl;
}
flip1( f, j, 42); //通过flip1调用f不会改变j
这个flip1函数在调用一个接受引用参数的函数时会出现问题。
定义能保持类型信息的函数参数
如果一个函数参数是指向模板类型参数的右值引用,它对应的实参的const属性和左/右值属性将得到保持。
template<typename F, typename T1, typename T2>
void flip2(F f, T1 &&t1, T2 &&t2)
{
f(t2, t1);
}
void g(int &&v1, int &v2)
{
cout << v1 << " " << ++v2 << endl;
}
flip( g, i, 42); //错误:不能从一个左值实例化int&&
这个版本的flip2解决了一半问题,但不能用于接受右值引用参数的函数。在flip2中i是int&,到g中v1应该是int&&,类型转换出错。
在调用中使用std::forward保持类型信息
我们可以使用一个名为forward的新标准库来传递参数,它能保持原始实参的类型。必须通过显式模板实参来调用,返回该显式实参类型的右值引用:
#include <utility>
template <typename Type> intermediary(Type &&arg)
{
finalFcn(std::forward<Type>(arg));
}
template<typename F, typename T1, typename T2>
void flip2(F f, T1 &&t1, T2 &&t2)
{
f(std::forward<T2>(t2), std::forward<T1>(t1));
}
与std::move相同,对std::forward不适用using声明是一个好主意