16.1定义模板
关于模板,第一个要提的点就是,这个字念mu模板而不是mo(一开始打字就发现拼音错了,突然觉得自己要会小学深造一下).
模板就是将一个特定场合使用的东西变成可以在多个场合使用的东西.
16.1.1函数模板
template<typename T>
bool bijiao(T &a,T &b){
return a==b;
}
上例是一个用于比较的函数,可以将T看成是int或是string之类的,总之T是代表一种类型,但是在我们编写代码的时候我们不知道这个T到底是什么类型,因此使用T来表示任意类型.如果我们需要比较两个int或是string,则可以
bijiao(1,2); //比较两个整数
bijiao("hello","world"); //比较两个字符串
如果没有模板,那么我们需要编写两个重载的函数:
bool bijiao(int &a,int &b){ return a==b;}
bool bijiao(string &a,string &b){ return a==b;}
并且如果后续有其他类型也需要使用该函数,则还需要再次编写.但如果使用了模板,则只需要编写一次即可.
template表示接下来的东西是一个模板.template也不一定要单独一行,你愿意的话也可以写成下面这样:
template<typename T> bool bijiao(T &a,T &b){
return a==b;
}
template后接尖括号<>,是模板参数列表,里面可以用逗号分隔写入多个模板参数.里面是typename T,其中关键字typename是固定写法,也可以写成class,当我们需要通知编译器一个名字表示类型时则必须使用typename,因此最好还是都写成typename.typename后面的T表示为未知类型的名称,不一定要写成T,可以随便给它起名字,但是以下我都用T来表示模板参数的名字.
当我们调用模板函数时,编译器会用过我们传入的参数类型来推断T是什么,以此来实例化出来一个函数.
若是传入的参数为自定义类型,则需要保证该类型支持模板函数内的操作,例如:
class people {
public:
int age;
string name;
people(int a,string n):age(a),name(n) {};
bool operator== (const people &p) { //需要支持==运算符
return this->age ==p.age;
}
};
template<typename T> bool bijiao(T &a,T &b){
return a == b;
}
int main(void) {
people p1=people(12, "a");
people p2=people(13, "b");
int a = 0; int b = 1;
cout << bijiao(a, b) << endl;
cout << bijiao(p1,p2) << endl;
return 0;
}
不过模板应当尽量减少对实参类型的需求.
使用模板函数时,通常编译器会在三个阶段报告错误.
编译模板:在编写阶段,编译器仅检查拼写错误,漏写分号等语法错误.
使用模板:在编写使用模板的代码时,会检查实参列表与模板参数列表的类型是否匹配.模板参数列表中,一个T(多个模板参数需要不同的名字用于区分)只能表示为一个类型,例如在上面的bijiao函数中传入两个类型不一致的实参则会发生错误.
模板实例化:在编译的时候,若是发现实参的类型(大多情况都和自定义类型有关)无法满足模板函数内部的操作时,则会报错.所以这种比较隐蔽的错误,在编写的时候IDE是不会有提示的.
16.1.2类模板
有函数模板,自然就有类模板,但是和函数模板不一样的是,类模板在使用时需要指定模板参数的类型.例如:
vector<int> v;
deque<int> q;
stack<int> s;
类模板的名字不是类型,而给类模板的模板参数指定了类型的才是类型.所以严格来说,vector并不是一个类型,而vector<int>才算一个类型.
当我们使用STL(标准模板库)里的容器时,需要指定类型,就是因为它们是模板.
编写类模板时和函数模板差不多一致,都是在开头加上template和模板参数列表.类模板参数列表里也可以有多个模板参数,参考pair.
template<typename T>
class test{
//具体内容
}
默认情况下, 对于一个实例化了的类模板,其成员只有在使用时才被实例化. 若类模板有友元,若友元不是模板,则友元可以访问改类的所有实例.若友元也是模板,则类模板可以指定友元的权限(访问本类的所有实例或是特定实例)
以下演示模板为友元,模板为非友元则和普通定义友元一致就不再演示.
template<typename T> class test; //这里两行是提前声明例子中用到的模板类和模板函数
template<typename T> void call(test<T>& t); //因为在实现的时候它们会相互包含,因此需要提前声明
template<typename T>
void call(test<T>& t) { //模板函数,用于打印出test<T>类型对象的值.
cout << t.val << endl;
}
template<typename T>
class test {
friend void call<T>(test<T> &t); //若留此行,则表示call这个函数是所有test实例的友元
friend void call<int>(test<int> &t); //若留此行,则表示call仅为test<int>的友元
public:
test(T v):val(v) {};
private:
T val;
};
int main(void) {
test<int> t1(1);
test<char> t2('a');
call(t1);
call(t2);
return 0;
}
16.1.3模板参数
模板参数的名字可以随意起,但通常使用T.若多个模板在同一个作用域下,则应当避免模板参数的名字起冲突.
声明模板时应当包含模板参数列表,可以参考上面的代码.
模板中通常包含模板,所以在含有模板的文件中,应当在文件开头就对模板进行声明,同样参考上面的代码.
早期的C++标准只允许类模板有默认实参.但现在,函数模板也可以拥有默认实参.
无论何时使用类模板,都需要在类模板名字后面加上模板参数列表,若是所有参数都有默认实参,并且我们需要使用所有的默认实参时,模板参数列表可为空,即只写上<>空的尖括号.
如下例子中的t2:
16.1.4成员模板
一个类(无论是不是模板)的成员都可以是模板.被称为成员模板,但成员模板不能是虚函数.
使用时同样可以指定模板实参指定的类型.例如
16.1.5控制实例化
当模板被使用时才会被实例化,若是编写大项目(有多个文件),其中每个文件都实例化了某个模板的某个示例,则会造成资源浪费.在新标准中可以通过显示实例化来避免这种开销.
在模板声明的开头加上关键字extern则为显示实例化.编译器遇到extern模板声明时则不会在本文件中生成实例化代码.则必须在程序的其他位置有一个非extern的声明(定义).
简而言之,在多文件程序中,只需要留一个模板定义,其他地方声明模板都可以在开头加上extern.
16.2模板实参推断
16.2.1类型转换与模板类型参数
使用模板函数时,如果函数参数不是模板参数,则会对实参进行正常的类型转换.若是形参类型使用了模板类型参数,则大多情况下不会对实参进行类型转换,而是生成一个新的模板实例,例如:
可以看到没有使用模板的函数会将传入的实参类型转变为形参的类型,而使用了模板的函数则会生成新的模板实例.
将实参传递给带模板类型的函数形参时,能够自动应用的类型转换只有const转换以及数组或函数到指针的转换.若是不理解这段话,可以参考下面书中给出的示例:
16.2.2函数模板显式实参
使用函数模板时同样可以和使用类模板一样进行显式实参(指定模板参数类型),不一样的是类模板强制要求显式实参,而函数模板可以根据需求选择是否显示实参.
还是上面的例子,在使用函数模板时显式实参,则可以达到图中效果.
显式模板实参按照从左到右的顺序与模板参数列表进行匹配:
//声明一个函数模板
template<typename T1,typename T2>
void test(T1 a,T2 b);
//使用函数模板并显式部分模板实参
test<int>(1.0,1);
//此时T1被显式指定为了int,所以即使传入的是1.0,在函数内仍被转换为int.
//而T2通过传入的实参1而被推断为int.
但如果模板参数列表无法全部被推断出具体类型,则必须通过显示实参来指定模板参数类型,例如函数返回值类型为模板参数,此时无法通过传入的实参来推断出,则必须显式指定模板参数类型.
16.2.3尾置返回类型与类型转换
如上小节所说,如果返回值类型使用了模板参数,则我们无法通过推断实参来判断返回值类型,则必须显式指定实参类型.但也有例外,如果返回值类型与形参类型一致,我们就可以通过尾置返回值类型和decltype(获取变量的类型,忘记的可以回顾一下前几章或者百度)配合,例如:
template<typename T1,tyename T2>
auto fun(T1 a,T2 b) -> decltype(a){
return a+b;
}
//当然,上述例子完全可以写成下面这样,上例只是一种启发
template<typename T1,tyename T2>
T1 fun(T1 a,T2 b){
return a+b;
}
当我们编写函数模板时,我们无法知道后面使用该函数模板的人会传入什么奇奇怪怪的类型,但是办法总比困难多,我们可以使用标准库的类型转换模板(需要include<type_traits>)来获取元素类型,然后针对不同类型进行不同操作.
可以结合下面的表和上面书中给出的例子参考一下(因为俺不太懂).例子中的typename是为了告诉编译器type为一个类型.
16.2.4函数指针和实参推断
若使用函数模板初始化函数指针或是为函数指针赋值,则编译器使用指针的类型来推断模板实参,若是不能从函数指针类型确定模板实参,则会报错.当参数是一个函数模板实例的地址时,程序上下文必须满足对每个模板实参,能够唯一确定其类型或值.
16.2.5模板实参推断和引用
若函数参数是模板类型参数的一个普通引用(类似于T&),则只能传递给它左值.但如果函数参数是类似 const T& 的话,则没有限制,可以传递左值也可以传递右值.当函数参数形如 T&& ,那也是可以传递右值的.
若是发生引用嵌套(引用的引用),可以对引用进行折叠.
16.2.6理解std::move
不要理解,去感受.
本小节简单剖析了标准库提供的std::move,感兴趣的小伙伴可以自行去看原书.
16.2.7转发
如果函数内部使用了同样的形参调用了其他函数,那么可以称作转发(不单指函数模板),例如:
template<typename T,typename T1>
void fun(T a,T1 b){
fun1(a,b);
}
在转发的同时,我们需要保证转发使用的参数类型与接收到的形参类型一致.
如果一个函数参数是指向模板类型参数的右值引用(T&&),则对应的实参的const属性和左/右值属性将得到保持,因此函数模板若是需要转发,则形参接收应当使用右值引用.
除此之外,使用std::forward标准库函数可以用于保持原始参数的类型,因此完整的函数模板转发例子如下:
16.3重载与模板
函数模板可以被另一个函数模板或是非模板函数重载,只需要保证名字相同,且形参列表不同即可.
当有多个重载模板对一个调用提供同样好的匹配时,应选择最特例化的版本.
若是非函数模板与一个函数模板提供同样好的匹配则优先选择非模板版本.
在定义任何函数之前,记得声明所有重载的函数版本,这样编译器就不会由于没遇到你希望调用的函数而实例化一个并非是需要的版本.
16.4可变参数模板
参数包是可变数目的参数,共有两种参数包:模板参数包和函数参数包.
使用省略号来表示参数包,例如:
如果想知道一个包里有多少参数,可以使用sizeof...
cout<<"包内有"<<sizeof...(args)<<"个参数"<<endl;//假设args为一个包
16.4.1编写可变参数函数模板
可变参数模板用于我们不知道所要处理的参数的数目也不知道其类型的情况.
16.4.2包扩展
这里的扩展意思就是将包拆开,获取包内单独的元素,操作方法就是在包的名字后面加上三个点...
以下面的例子为例(是上一小节书中的例子)
其中Ts是模板参数包,b是函数参数包.第一次使用包扩展(拆包)是在第二个print函数的参数列表里,b的类型为Ts...即将模板参数包拆开.
第二次使用是在第二个print函数内,递归调用了print函数,而递归时只传入了拆开的函数参数包b(这里不算转发).因此每次调用都会使参数减少,直到参数只有一个时,函数参数包b表示为0个参数,此时调用的是第一个print函数(因为只有一个参数时,它匹配度更高),结束递归.
16.4.3转发参数包
可变参数模板+forward转发=
std::forward<Ts>(b)... //其中Ts表示模板参数包,b表示函数参数包
16.5模板特例化
假设我们之前的模板是这样的:
template<typename T>
void fun(T a,T b);
模板特例化就是:
template<>
void fun(int a,int b);
将模板参数列表清空,然后在原本模板参数的位置写上自己需要的特例,但是要记得和模板匹配,例如本例中模板参数只有一个T,并且形参a和b的类型都是T.所以实例化要保证形参a和b的类型一致.
特例化的本质是实例化一个模板,而不是重载,因此特例化不影响函数匹配(不用担心重复声明).
我们可以部分特例化类模板,但是不能部分特例化函数模板.