模板本身不是可编译的代码,而是用来指导编译器生成可编译代码的文本。
1 函数模板参数
函数模板参数可以根据模板实参自动推导,也就是说可以从实参自动推导的模板参数可以省略书写,但是要注意以下几个规则:
1) 编译器只根据函数调用时给出的实参列表推导模板参数值,与函数参数无关的模板参数无法推导
2) 与函数返回值相关的模板参数其值也无法推导
3) 所有可推导模板参数必须是连续位于模板参数列表尾部,中间不能有不可推导的模板参数
一个例子如下:
#include <iostream>
template<typename T0,typename T1,typename T2,typename T3,typename T4>
T2 func(T1 v1, T3 v3, T4 v4);//模板参数中可函数函数关联的是T1 T3 T4
int main() {
double sv2;
using namespace std;
sv2 = func<double, int, int>(1, 2, 3);//<T0,T1,T2>,T3 T4是由函数参数自动推导而来的
cout << "\tsv2: " << sv2 << endl; //v1: 1v3: 2 v4: 3 || sv0: 0 sv2: 0
sv2 = func<double, int, int>(1, 2, 3);
cout << "\tsv2: " << sv2 << endl; //v1: 1v3: 2 v4: 3 || sv0: -1 sv2: -1
sv2 = func<double, int, int>(1, 0.1, 0.1);
cout << "\tsv2: " << sv2 << endl; //v1: 1v3: 0.1 v4: 0.1 || sv0: 0 sv2: 0
sv2 = func<int, double, double>(0.1, 0.1, 0.1);
cout << "\tsv2: " << sv2 << endl; //v1: 0.1v3: 0.1 v4: 0.1 || sv0: 0 sv2: 0
}
template<typename T0,typename T1,typename T2,typename T3,typename T4>
T2 func(T1 v1, T3 v3, T4 v4){
T0 static sv0 = T0(0);//这里使用静态成员考察静态成员在模板函数中是否像普通函数中一样只有一份值
T2 static sv2 = T2(0);
std::cout << "\tv1: " << v1<< "\tv3: " << v3<< "\tv4: " << v4<< "\t|| sv0: " << sv0;
T2 v2 = sv2;
sv0 -= 1;
sv2 -= 1;
return v2;
}
说明:模板参数中T0满足1)故不能通过函数参数推导出,T2满足2),因此T0和T2在模板实例化时都不能省略,由于T1在T2之前故也不能省略(C++不支持跳过T1的赋值)。故上面三种实例化的都是实例化<T0,T1,T2>而T3和T4是由函数的实参自动推导而来的。
C++11允许模板函数可以和模板类一样具有模板参数默认值,故可以将T0和T2声明一个默认值,如下:
template<typename T0,typename T1,typename T2,typename T3,typename T4>
T2 func(T1 v1,T3 v3,T4 v4);
fun(1,2,3);//通过给T0和T2指定了默认的模板参数值后,此处所有的模板参数都可以省略书写了
fun(1,'a','b');
模板函数中的静态变量是否会像普通函数中的静态变量只有一份?通过输出可以看出,sv0和sv2值在前两次func调用时由0变到-1,这是因为前两次调用实例化模板都是func<double,int,int,int>有一份共同的实例化函数体。当有多个调用使用相同的模板参数值时,编译器只为此模板参数值生成同一函数而将不同调用都链接在同一函数上,只要有任意模板参数值不同编译器就会生成不同的函数体以供链接。总之,编译器为同一组模板参数值只生成唯一函数体。
假设上面文件编译g++ test.cpp 那么查看a.out文件:nm -C a.out |grep func |grep W 输出如下,只有三个模板实例化函数体,可见前两次func共享func<double,int,int,int>:
0000000000400b94 W int func<double, int, int, double, double>(int, double, double)
0000000000400acf W int func<double, int, int, int, int>(int, int, int)
0000000000400c73 W double func<int, double, double, double, double>(double, double, double)
2 模板文件组织
通常的C++工程组织都是将函数声明之类的放在.hpp中,函数实现放在.cpp中。但是对于模板来说这样组织会导致连接错误,因为.hpp中只是模板的声明,.cpp中是模板的实现并没有任何模板实例化的语句故不会生成任何模板实例化代码,那么在其它.cpp中实例化模板时导致无法找到模板代码导致链接错误。
当一个工程中连接多个文件时可能出现相同的模板实例化代码,如下:
//======================================
//文件名caller1.cpp
#include <iostream>
template<typename T>
void func(T const &v)
{
std::cout << "func1: " << v << std::endl;
}
void caller1() {
func(1);//func<int>
func(0.1);//func<double>
}
//======================================
//文件名caller2.cpp
#include <iostream>
template<typename T>
void func(T const &v)
{
std::cout << "func2: " << v << std::endl;
}
void caller2() {
func(2);//func<int>
func(0.2f);//func<float>
}
//======================================
//文件名main.cpp
void caller1();
void caller2();
int main()
{
caller1();
caller2();
return 0;
}
程序输出:
func1: 1
func1: 0.1
func1: 2
func2: 0.2
可见上面的caller1()和caller2()都实例化了同一个func<int>,且都是使用caller1的func。因为编译器会根据函数名、模板实参列表、参数列表将弃用等价的模板函数(尽管它们的函数体不一样),而弃用哪个文件的函数是随机的,可能与文件的连接顺序有关。
这样就导致了一个问题,不同函数体实现的函数被弃用了,可能导致一些截然不同的结果,怎样避免呢?尽量采用不同的函数名,采用命名空间隔离。但是不要强制采用using namespace这样暴力的方式将命名空间所有的元素包含进另一个命名空间,这样可能会造成如上的函数重复实例。采用using 引用函数。
这里提下 using在C++11中的两个用途:
1) 引用,如下:
class base{
public:
void fun();
};
class derived:public base{
public:
using base::fun;//使用基类的fun函数
};
2) 自定义功能,完全替代了typedef,如下:
using size_t=decltype(sizeof(0));//通过decltype自动推导功能实现了size_t跨平台的移植
3 外部模板
C++11已经弃用了export语义,即在不同文件中在template前加上export就可以实例化模板。C++11的extern作为外部模板语义,如下:
//文件test.hpp
template<typename T>
void fun();
//文件test1.cpp
template void fun<int>(int);//显示实例化出了一个fun<int>的版本
//文件test2.cpp
extern template void fun<int>(int);//外部模板的声明
通过上面的声明方式,test1.cpp和test2.cpp编译连接后fun都是共享test1.cpp的模板实例化代码。