C++ 函数模板
所谓模板,就是模板,但与我们所熟知的模子不同,模板用于生成不同的东西(函数或者类)。在C++中有函数模板和类模板。今天我们来探讨一下函数模板。
场景:我们需要一个函数来做加法运算又或者我们需要自己实现print函数,那怎么处理传入参数不同(double ? int ? char ? short ? string ?)的情况呢?
你或许会脱口而出“函数重载”,但这意味着你至少要写两个以上的函数去支持你的参数类型和个数的变化,right?而且这种做法不支持扩展,如果有其他的需求,就必须再增加重载函数,这是比较愚蠢的,相信你也这么认为。那么,更好的办法是什么呢?函数模板。
我们来看例子👇
1 template <typename T>//先声明模板参数 T 2 typename T add(const T &num1, const T &num2)//定义模板函数,注意参数的类型 3 { 4 return num1 + num2; 5 } 6 7 int main() 8 { 9 cout << add(1, 3) << endl;//模板的实例化:int add(const int &num1, const int &num2) 10 cout << add(3.0, 9.9) << endl; //实例化:double add(const double &num1, const double &num2)
11 return 0; 12 }
这样一来,我们就实现了一个模板,多种参数类型,从这样说来,模板是优于函数重载的。那么,使用他的时候,编译器做了哪些工作呢?
1. 编译模板本身时, 检查语法信息;
2. 模板使用时,检查参数类型是否匹配,数目正确与否;
3. 实例化时,进行实参推断。
👉还记得auto和decltype吗? 这里的实参推断与他们类似。如上面例子中的add(1, 3),我们的实参就是int型,那么编译器就会将int型绑定到我们的模板类型T上,这样一来,模板就实例化成了
int add(const int &num1, const int &num2)
{
return num1 + num2;
}
好处是,编译器自己进行参数类型推断,生成函数实例,无需人为参与,也避免了,函数重载的代码堆砌。(personal thought)一些我们使用已久的函数不就是这样吗?sort(),find(),reverse()...
下面我们讨论一下,模板的(全)特例化与偏特例化。
【1】全特化
所谓特例化,就是将参数定死。它本质上是模板的一个特例。比如:
templete <>//标识我们在特例化模板
double add(const double &d1, const double &d2)//参数类型具体化 { return d1 + d2; }
这里我们就接管了部分编译器的工作,如果同时存在模板和特例化模板, 当我们使用模板(参数与特例化一致),编译器会优先选择特例化模板实例(至少避免了类型推断不是吗?)当我们有更好更直接的函数时,要毫不犹豫地使用它。(比如:我们对传入参数所知甚少时,就要尽量“包住“所有的可能输入;而当我们明确知道参数类型和数量时,就应该特化它,提高效率)
【2】偏特化
偏特化是指,特化部分参数(个数特化)或者参数类型(范围特化)。
个数特化:将几个参数类型具体化
1 template <typename T> 2 typename T add(const T &num1, const int &num2)//特化一个参数 为int 3 { 4 return num1 + num2; 5 }
范围特化:将参数限定在某个范围内(指针、const ...)
1 template <typename T> 2 typename T add(const int num1, const T &num2)//将一个参数特化为 const int 3 { 4 return num1 + num2; 5 }
1 template <typename T> 2 typename T add(const int* num1, const T &num2)//将一个参数特化为 const int* 指针 3 { 4 return num1 + num2; 5 }
至于是否进行特例化,就要自己想想了。
1. 某些传入参数没有和其他参数一样的特性(算术或者逻辑),我们就需要为它量身定制一个模板实例;(就好比add()例子中,要加和string怎么办?它可是字符数组呀。)
2. 某些参数我们是可以确定的,就可以特例化,以期更高的效率(避免类型推导和生成实例)。
类模板更为复杂,可以参考这里