模板实参推断
我们已经看到,对于函数模板,编译器利用调用中的函数实参来确定其模板参数。
从函数实参来确定模板实参的过程被称为模板实参推断。
也就是说,只有函数参数才配有模板实参推断,函数返回类型是不配有的
在模板实参推断过程中, 编译器使用函数调用中的实参类型来寻找模板实参,用这些模板实参生成的函数版本与给定的函数调用最为匹配。
类型转换与模板类型参数
与非模板函数一样,我们在一次调用中传递给函数模板的实参被用来初始化函数的形参。
如果一个函数形参的类型使用了模板类型参数,那么它采用特殊的初始化规则。
只有很有限的几种类型转换会自动地应用于这些实参。
编译器通常不是对实参进行类型转换,而是生成一个新的模板实例。
与往常一样,顶层const无论是在形参中还是在实参中,都会被忽略。
在其他类型转换中,能在调用中应用于函数模板的包括如下两项。
- const转换:可以将一个非const对象的引用(或指针)传递给一个const的引用(或指针)形参。
- 数组或函数指针转换:如果函数形参不是引用类型,则可以对数组或函数类型的实参应用正常的指针转换。一个数组实参可以转换为一个指向其首元素的指针。类似的,一个函数实参可以转换为一个该函数类型的指针。
其他类型转换,如算术转换、派生类向基类的转换以及用户定义的转换,都不能应用于函数模板。
作为一个例子,考虑对函数fobj和fref的调用。fobj函数拷贝它的参数,而fref的参数是引用类型:
#include<iostream>
using namespace std;
template <typename T>
T fobj(T a, T b)
{
cout << "调用 fobj(T,T)" << endl;
return a;
}// 实参被拷贝
template <typename T>
T fref(const T& a, const T& b)
{
cout << "调用fref (const T&, const T&)" << endl;
return a;
}// 引用
int main()
{
string s1("a value");
const string s2("another value");
fobj(s1, s2); //调用 fobj(string, string);const被忽略
fref(s1, s2); // 调用fref (const string&, const string)
//将s1转换为const是允许的
int a[10], b[42];
fobj(a, b); //调用f(int*, int*)
fref(a, b);//错误:数组类型不匹配
}
在第一对调用中,我们传递了一个string和一个const string。虽然这些类型不严格匹配,但两个调用都是合法的。
在fobj调用中,实参被拷贝,因此原对象是否是const没有关系。
在fxef调用中,参数类型是const的引用。对于一个引用参数来说,转换为const是允许的,因此这个调用也是合法的。
在下一对调用中,我们传递了数组实参,两个数组大小不同,因此是不同类型。
在fobj 调用中,数组大小不同无关紧要。两个数组都被转换为指针。fobj中的模板类型为int*。
但是,fref调用是不合法的,如果形参是一个引用,则数组不会转换为指针。a和b的类型是不匹配的,因此调用是错误的。
将实参传递给带模板类型的函数形参时,能够自动应用的类型转换只有const转换及数组或函数到指针的转换。
使用相同模板参数类型的函数形参
一个模板类型参数可以用作多个函数形参的类型。
由于只允许有限的几种类型转换,因此传递给这些形参的实参必须具有相同的类型。如果推断出的类型不匹配,则调用就是错误的。
例如,我们的compare函数接受两个const T参数,其实参必须是相同类型:
template<class T>
void compare(T a,T b)
{}
int main()
{
long lng;
compare(lng, 1024);//错误:不能实例化compare(long, int)
}
此调用是错误的,因为传递给compare的实参类型不同。
从第一个函数实参推断出的模板实参为long,从第二个函数实参推断出的模板实参为int。这些类型不匹配,因此模板实参推断失败。
如果希望允许对函数实参进行正常的类型转换,我们可以将函数模板定义为两个类型参数:
template<class A,class B>
void compare(A a,B b)
{}
现在用户可以提供不同类型的实参了:
long lng;
compare(lng, 1024);// 正确:调用 compare(long, int)
正常类型转换应用于普通函数实参
函数模板可以有用普通类型定义的参数,即,不涉及模板类型参数的类型。
这种函数实参不进行特殊处理;它们正常转换为对应形参的类型。
例如,考虑下面的模板:
template <typename T>
ostream &print(ostream sos, const T &obj)
{return os << obj;}
第一个函数参数是一个已知类型ostream&。第二个参数obj则是模板参数类型。
由于os的类型是固定的,因此当调用print时,传递给它的实参会进行正常的类型转换:
print(cout, 42);// 实例化print(ostream, int)
ofstream f ("output");
print(f,10); //使用print (ostream&, int);将f转换为ostream&
在第一个调用中,第一个实参的类型严格匹配第一个参数的类型。此调用会实例化接受一
个ostream&和一个int的print版本。
在第二个调用中,第一个实参是一个ofstream,它可以转换为ostream&。由于此参数的类型不依赖于模板参数,因此编译器会将f隐式转换为ostream。
#include<iostream>
#include<fstream>
using namespace std;
template <typename T>
ostream& print(ostream &os, const T& obj)
{
return os << obj;
}
int main()
{
print(cout, 42);// 实例化print(ostream, int)
ofstream f ("output");
print(f,10); //使用print (ostream&, int);将f转换为ostream&
}
如果函数参数不是模板参数,则对实参进行正常的类型转换
函数模板显式实参
在某些情况下,编译器无法推断出模板实参的类型。
其他一些情况下,我们希望允许用户控制模板实例化。
当函数返回类型与参数列表中任何类型都不相同时,这两种情况最常出现。
指定显式模板实参
作为一个允许用户指定使用类型的例子,我们将定义一个名为sum 的函数模板,它接受两个不同类型的参数。
我们希望允许用户指定结果的类型。这样,用户就可以选择合适的精度。
我们可以定义表示返回类型的第三个模板参数,从而允许用户控制返回类型:
//编译器无法推断T1,它未出现在函数参数列表中
template <typename T1, typename T2, typename T3>
T1 sum(T2, T3)
{
}
在本例中,没有任何函数实参的类型可用来推断T1的类型。
每次调用sum时调用者都必须为T1提供一个显式模板实参。我们提供显式模板实参的方式与定义类模板实例的方式相同。
显式模板实参在尖括号中给出,位于函数名之后,实参列表之前
此调用显式指定T1的类型。而T2和T3的类型则由编译器从i和lng的类型推断出来。
显式模板实参按由左至右的顺序与对应的模板参数匹配;第一个模板实参与第一个模板参数匹配,第二个实参与第二个参数匹配,依此类推。
只有尾部(最右)参数的显式模板实参才可以忽略,而且前提是它们可以从函数参数推断出来。(意思就是返回类型不能忽略,因为它不能从函数参数中推导出来)
如果我们的 sum 函数按照如下形式编写:
// 糟糕的设计:用户必须指定所有三个模板参数
template <typename T1, typename T2, typename T3>
T3 alternative_sum(T2, T1){}
则我们总是必须为所有三个形参指定实参:
//错误:不能推断前几个模板参数
auto val3 = alternative_sum<long long>(i, lng);
// 正确:显式指定了所有三个参数
auto va12 = alternative_sum<long long, int, long>(i, lng);
这是因为只有最尾部(最右)参数的显式模板实参才可以忽略,而且前提是它们可以从函数参数中推导出来
第一个调用忽略了最右边那个模板实参,即T3,但是T3是函数返回类型,是不能提供函数参数推断出来的,所以这个不能省略
正常类型转换应用于显式指定的实参
对于用普通类型定义的函数参数,允许进行正常的类型转换。
出于同样的原因,对于模板类型参数已经显式指定了的函数实参,也进行正常的类型转换:
#include<iostream>
using namespace std;
template <typename T>
T compare(T a, T b)
{
return a;
}
int main()
{
long lng=8;
compare(lng, 1024); // 错误:模板参数不匹配
compare<long>(lng, 1024); // 正确:实例化compare(long, long)
compare<int>(lng,1024); // 正确:实例化compare(int, int)
}
如我们所见,第一个调用是错误的,因为传递给compare的实参必须具有相同的类型。
如果我们显式指定模板类型参数,就可以进行正常类型转换了。
因此,调用compare<long>等价于调用一个接受两个const long&参数的函数。int类型的参数被自动转化为long。在第三个调用中,T被显式指定为int,因此lng被转换为int
尾置返回类型与类型转换
当我们希望用户确定返回类型时,用显式模板实参表示模板函数的返回类型是很有效的。
但在其他情况下,要求显式指定模板实参会给用户增添额外负担,而且不会带来什么好处。
例如,我们可能希望编写一个函数,接受表示序列的一对迭代器和返回序列中一个元素的引用:
template <typename It>
??? &fen(It beg, It end)
{
// 处理序列
return *beg;//返回序列中一个元素的引用
}
我们并不知道返回结果的准确类型,但知道所需类型是所处理的序列的元素类型;
vector<int> vi = { 1, 2, 3, 4, 5 };
vector<string> ca = {"hi", "bye"};
auto& i = fcn(vi.begin(),vi.end());// fcn应该返回int&
auto& s = fcn(ca.begin(), ca.end());// fcn应该返回string
此例中,我们知道函数应该返回*beg,而且知道我们可以用decltype(*beg)来获取此表达式的类型。
但是,在编译器遇到函数的参数列表之前,beg都是不存在的。为了定义此函数,我们必须使用尾置返回类型。由于尾置返回出现在参数列表之后,它可以使用函数的参数:
template <typename It>
auto fcn(It beg, It end) -> decltype(*beg)
{// 处理序列
return *beg;// 返回序列中一个元素的引用
}
此例中我们通知编译器fcn的返回类型与解引用beg参数的结果类型相同。
解引用运算符返回一个左值,因此通过decltype推断的类型为beg表示的元素的类型的引用。
因此,如果对一个 string 序列调用fcn,返回类型将是string&。如果是int序列,则返回类型是int&。
进行类型转换的标准库模板类
有时我们无法直接获得所需要的类型。
例如,我们可能希望编写一个类似 fcn的函数,但返回一个元素的值而非引用。
在编写这个函数的过程中,我们面临一个问题:对于传递的参数的类型,我们几乎一无所知。
在此函数中,我们知道唯一可以使用的操作是迭代器操作,而所有迭代器操作都不会生成元素,只能生成元素的引用。
为了获得元素类型,我们可以使用标准库的类型转换模板。这些模板定义在头文件type_traits中,这个头文件中的类通常用于所谓的模板元程序设计。类型转换模板在普通编程中也很有用。
在本例中,我们可以使用remove_reference来获得元素类型。
remove reference模板有一个模板类型参数和一个名为type的(public)类型成员。如果我们用一个引用类型实例化remove reference,则type将表示被引用的类型。
例如,如果我们实例化 remove reference<int&>,则type成员将是int。类似的,如果我们实例化remove reference<string&>,则type成员将是string,依此类推。更一般的,给定一个迭代器 beg:
remove_reference<decltype (*beg)>::type
将获得 beg 引用的元素的类型:decltype(*beg)返回元素类型的引用类型,remove_reference::type脱去引用,剩下元素类型本身。
组合使用remove_reference、尾置返回及decltype,我们就可以在函数中返回元素值的拷贝:
//为了使用模板参数的成员,必须用typename
template <typename It>
auto fcn2(It beg, It end) -> typename remove_reference<decltype(*beg)>::type
{
//处理序列
return *beg; //返回序列中一个元素的拷贝
}
注意,type 是一个类的成员,而该类依赖于一个模板参数。因此,我们必须在返回类型的声明中使用typename来告知编译器,type表示一个类型
对 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_pointer | 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 |
表中描述的每个类型转换模板的工作方式都与remove_reference类似。
每个模板都有一个名为type的public成员,表示一个类型。
此类型与模板自身的模板类型参数相关,其关系如模板名所示。
如果不可能(或者不必要)转换模板参数,则type成员就是模板参数类型本身。
例如,如果T是一个指针类型,则remove_pointer<T>::type是T指向的类型。如果T不是一个指针,则无须进行任何转换,从而type具有与T相同的类型
函数指针和实参推断
当我们用一个函数模板初始化一个函数指针或为一个函数指针赋值时,编译器使用指针的类型来推断模板实参。
例如,假定我们有一个函数指针,它指向的函数返回int,接受两个参数,每个参数都是指向const int的引用。
我们可以使用该指针指向compare的一个实例:
template <typename T>
int compare(const T&, const T&){}
// pf1指向实例 int compare(const int&, const int&)
int (*pfl) (const int&, const int&) = compare;
pf1中参数的类型决定了T的模板实参的类型。在本例中,T的模板实参类型为int。指针pfl指向compare的int版本实例。如果不能从函数指针类型确定模板实参,则产生错误:
#include <string>
using namespace std;
template <typename T>
int compare(const T& a, const T& b) {
// ... some comparison logic ...
return 0;
}
int main() {
// 下面是两个func的重载版本
void func(int(*)(const string&, const string&));
void func(int(*)(const int&, const int&));
func(compare); // 或者 func(compare<int>);
return 0;
}
void func(int(*)(const string&, const string&)) {}
void func(int(*)(const int&, const int&)) {}
这段代码的问题在于,通过func的参数类型无法确定模板实参的唯一类型。对func的调用既可以实例化接受int的compare版本,也可以实例化接受string的版本。由于不能确定 func的实参的唯一实例化版本,此调用将编译失败。
我们可以通过使用显式模板实参来消除func调用的歧义:
// 正确:显式指出实例化哪个 compare版本
func (compare<int>);// 传递 compare(const int&, const int&)
此表达式调用的func版本接受一个函数指针,该指针指向的函数接受两个const int&参数。
#include <string>
using namespace std;
template <typename T>
int compare(const T& a, const T& b) {
// ... some comparison logic ...
return 0;
}
int main() {
// 下面是两个func的重载版本
void func(int(*)(const string&, const string&));
void func(int(*)(const int&, const int&));
func(compare<int>); // 或者 func(compare<int>);
return 0;
}
void func(int(*)(const string&, const string&)) {}
void func(int(*)(const int&, const int&)) {}
当参数是一个函数模板实例的地址时,程序上下文必须满足:对每个模板参数,能唯一确定其类型或值。
模板实参推断和引用
为了理解如何从函数调用进行类型推断,考虑下面的例子:
template <typename T> void f(T &p);
其中函数参数p是一个模板类型参数T的引用,非常重要的定记住两点:
- 编译器会应用正常的引用绑定规则;
- const是底层的,不是顶层的。
从左值引用函数参数推断类型
当一个函数参数是模板类型参数的一个普通(左值)引用时(即,形如T&),
绑定规则告诉我们,只能传递给它一个左值(如,一个变量或一个返回引用类型的表达式)。
实参可以是const类型,也可以不是。如果实参是const的,则T将被推断为const类型。
#include <string>
using namespace std;
template <typename T> void f1(T&) {}// 实参必须是一个左值
// 对fl的调用使用实参所引用的类型作为模板参数类型
int main() {
int i;
const int ci = 1;
f1(i); // i是一个int;模板参数类型T是int
f1(ci); // ci是一个const int;模板参数T是const int
f1(5); // 错误:传递给一个&参数的实参必须是一个左值
}
如果一个函数参数的类型是const T&,正常的绑定规则告诉我们可以传递给它任何类型的实参——一个对象(const或非const)、一个临时对象或是一个字面常量值。
当函数参数本身是const时,T的类型推断的结果不会是一个const类型。
const已经是函数参数类型的一部分;因此,它不会也是模板参数类型的一部分:
template <typename T> void f2(const T&) {}//可以接受一个右值
// f2 中的参数是const & ;实参中的const是无关的
// 在每个调用中,f2的函数参数都被推断为 const int&
int main() {
int i;
const int ci = 1;
f2(i); // i是一个int;模板参数T是int
f2(ci);// ci是一个const int,但模板参数T是int
f2(5); // 一个const & 参数可以绑定到一个右值;T是int
}
从右值引用函数参数推断类型
当一个函数参数是一个右值引用(即,形如T&&)时,正常绑定规则告诉我们可以传递给它一个右值。
当我们这样做时,类型推断过程类似普通左值引用函数参数的推断过程。
推断出的T的类型是该右值实参的类型:
template<typename T>
void f3(T&&) {}
f3(42);//实参是一个int类型的右值;模板参数T是int
引用折叠和右值引用参数
假定i是一个int对象,我们可能认为像f3(i)这样的调用是不合法的。毕竟,是一个左值,而通常我们不能将一个右值引用绑定到一个左值上。
但是,C++语言在正常绑定规则之外定义了两个例外规则,允许这种绑定。这两个例外规则是move这种标准库设施正确工作的基础。
第一个例外规则影响右值引用参数的推断如何进行。
当我们将一个左值(如i)传递给函数的右值引用参数,且此右值引用指向模板类型参数(如T&&)时,编译器推断模板类型参数为实参的左值引用类型。
因此,当我们调用f3(i)时,编译器推断T的类型为int&,而非int.
template<typename T>
void f3(T&&) {}
int i;
f3(i);
T被推断为int&看起来好像意味着f3的函数参数应该是一个类型int 的右值引用
通常,我们不能(直接)定义一个引用的引用。但是,通过类型别名或通过模板类型参数间接定义是可以的。
在这种情况下,我们可以使用第二个例外绑定规则:如果我们间接创建一个引用的引用,则这些引用形成了“折叠”。在所有情况下(除了一个例外),引用会折叠成一个普通的左值引用类型。
在新标准中,折叠规则扩展到右值引用。
只在一种特殊情况下引用会折叠成右值引用:右值引用的右值引用。
总的来说,即,对于一个给定类型X;
- X& &,X& &&和X&& &都折叠成类型X&
- 类型X&& &&折叠成X&&
引用折叠只能应用于间接创建的引用的引用,如类型别名或模板参数。
如果将引用折叠规则和右值引用的特殊类型推断规则组合在一起,则意味着我们可以对一个左值调用f3。
当我们将一个左值传递给f3 的(右值引用)函数参数时,编译器推断T为一个左值引用类型:
template<typename T>
void f3(T&&) {}
int i;
const int ci = 1;
f3(i);// 实参是一个左值;模板参数T是int &
f3(ci);// 实参是一个左值;模板参数T是一个 const int&
当一个模板参数T被推断为引用类型时,折叠规则告诉我们函数参数T&&折叠为一个左值引用类型。
例如,f3(i)的实例化结果可能像下面这样:
//无效代码,只是用于演示目的
void f3<int&>(int& &&);// 当T是int&时,函数参数为int& &&
f3的函数参数是T&&且T是int&,因此T&&是int&&&,会折叠成int&。
因此,即使f3的函数参数形式是一个右值引用(即,T&&),此调用也会用一个左值引用类型(即,int&)实例化f3:
void f3<int&>(int&);//当少是int&时,函数参数折登为int&
这两个规则导致了两个重要结果:
- 如果一个函数参数是一个指向模板类型参数的右值引用(如,T&&),则它可以被绑定到一个左值;且
- 如果实参是一个左值,则推断出的模板实参类型将是一个左值引用,且函数参数将做实例化为一个(普通)左值引用参数(T&)
另外值得注意的是,这两个规则暗示,我们可以将任意类型的实参传递给T&&类型的函数参数。对于这种类型的参数,(显然)可以传递给它右值,而如我们刚刚看到的,也可以传递给它左值。
如果一个函数参数是指向模板参数类型的右值引用(如,T&&),则可以传递给它任意类型的实参。如果将一个左值传递给这样的参数,则函数参数被实例化为一个普通的左值引用(T&)
编写接受右值引用参数的模板函数
模板参数可以推断为一个引用类型,这一特性对模板内的代码可能有令人惊讶的影响:
template <typename T>
void f3(T&& val)
{
T t = val ://拷贝还是排定一个引用 ?
t = fcn(t);// 赋值只改变t还是既改变t又改变val?
if (val == t) (/*...*/)//若T是引用类型,则一直为true
}
当我们对一个右值调用f3时,例如字面常量42,T为int。在此情况下,局部变量t的类型为int,且通过拷贝参数val的值被初始化。当我们对t赋值时,参数val保持不变。
另一方面,当我们对一个左值i调用f3时,则T为int&。当我们定义并初始化局部变量t时,赋予它类型int&。因此,对t的初始化将其绑定到val。当我们对t赋值时,也同时改变了val的值。在f3的这个实例化版本中,if判断永远得到true。
当代码中涉及的类型可能是普通(非引用)类型,也可能是引用类型时,编写正确的代码就变得异常困难(虽然remove_reference这样的类型转换类可能会有帮助)。
在实际中,右值引用通常用于两种情况:模板转发其实参或模板被重载。
目前应该注意的是,使用右值引用的函数模板通常使用下面这种方式来进行重载:
template<typename T> void f(T&&); //绑定到非const右值
template <typename T> void f(const T&);// 左值和const右值
与非模板函数一样,第一个版本将绑定到可修改的右值,而第二个版本将绑定到左值或const右值。