1、通过一个简单的例子来理解模板的用途:
模板为不同类型的数据生成操作相同或相似的函数。
弱语言如Python,可以使用一种函数来应对各种类型,但是C++就不得不为不同的类型编写相似的函数。模板的作用就是把这一步骤交给编译器去执行,让这些函数在编译器生成。
2、模板参数的自动推导
原则:凡是可以推导出来的模板参数“值”就无需在模板实参列表中写明。
规则一:编译器值根据函数调用时给出的实参列表来推导模板参数值,与函数参数类型无关的模板参数无法推导
规则二:与函数返回值相关的模板参数其值也无法推导
规则三:所有可以推导模板参数必须是连续位于模板参数列表尾部,中间不能有不可退到的模板参数。
举例:
test1~test3的分析过程如下:
第一,sv2是返回值,我们不能通过返回值的类型推导模板参数的值,所以T2无法推导出来;
第二,T0使用的地方是函数内部的一个变量,也不是函数的参数,所以无法通过sv0的类型推导出T0的值
第三,以test1为例,func的三个参数分别是1,2,3整型,所以可以推断出T1、T3、T4是int。因为T0和T2无法推导出,所以必须在func<>中明确给出。而由于要根据声明的顺序给出,不能跳过T1,所以func<>中的三个类型分别是T0、T1、T2的类型。
3、模板参数的默认值
形如:
有两个具有默认类型,其它三个可以从函数参数类型推导,所以不需要尖括号:
4、模板参数的静态变量
这一讲用来说明,如果模板参数的值是相同的,那么模板函数实例就只生成一个,不会重复生成。
test1和test2中的static变量进行了递减,说明test1和test2中的func是同一个。这也就说明,func的实例是依具体的参数而决定的,如果模板参数值一样,编译器就不会重复生成函数实例。
我们逆向也可以知道,这里的两个函数是相同的:
用IDA加载进符号表之后,会更清晰一点:
5、问题一:模板函数应该如何在头文件里声明?
如果只是在头文件里添加一个函数模板的声明:
这样编译工程是肯定能编译通过的。因为,编译器在编译test.h和test.cpp文件时,只是读到了func0函数模板的实现,并没有读到任何需要生成函数模板的实例的语句,所以不会生成任何func0函数实例。
但是如果在main.cpp中添加了func0,那么此时就要生成一个func0的具体实例了,但test.h文件中只有一个func0函数模板的声明,编译器并没有生成实际的函数实例,只好在mian函数中的func0处预留一个链接调用,等待在链接过程中找到函数实现。因为你这里调用时,实际上是调用一个func0<int>(0); 但是test.cpp里并没有这个函数的实力。就会报错(这种错误很隐晦,很难排查):
所以,我们的解决办法是明确地在头文件中声明具体的实例。编译器就会生成这个函数模板实例了:
问题二:
但是,如果你想再生成func0(float)、func0(char)那么就得在头文件中添加两个实例的声明。但这貌似违背了最初使用模板的目的。
解决的办法就是把模板的实现也包含到头文件中,这样main函数把test.h包含进来了,编译器在编译时就知道main函数要用到一个func0<int>实例了,就可以编译出func0<int>实例了。//如果你把代码放到cpp里,那就是等到连接时去做事儿,如果放到h文件里,那就是在编译时去做事儿。
问题三:
产生重复模板实例问题。但这个问题会由连接器解决。
比如,新增加caller.obj
原有的main.obj中也有
但我们会发现main和caller.obj最终调用的都是caller中的func0:
就是因为连接器把caller0和main中的func0合并成了一个,合并规则是函数名、模板实参列表以及参数列表相同。