函数模板
什么是函数模板
C++的函数模板提供了一种方法,让编译器根据你写的模板,来自动按需生成函数。这个模板就称之为函数模板。函数模板格式如下:
template <typename AnyType>
void MyFunctionTemplate(AnyType ¶m)
{
}
这里AnyType就是任意数据类型,既包括语言内置的基础数据类型(char
、int
、double
),又包括自定义的数据类型,比如类,结构体,枚举,数组等。
为什么需要函数模板
假设我需要一个方法,打印给定数据类型的长度。第一时间想到的是重载函数来实现这个功能。
重载函数的缺陷
-
函数定义可能存在冗余
假定
printTypelength
方法在一个单独的模块内,将被我编译为动态库供别人使用,那么我就必须提前把所有要处理的类型都给实现了。如果我的应用程序如下:
int main(void)
{
char c;
printTypelength(c);
return 0;
}
那么我代码中实际仅需要针对char
的重载函数即可,其他几十个重载函数完全用不上,可执行程序的就会变小了。函数重载并未按需实现,而是一股脑的定义一堆你需要的重载函数。
-
可重用性不高
假设某天我突然定义了一个新的自定义数据类型,想要使用模块的
printTypelength
函数,这时我不得不在printTypelength源码里面新增对应的重载函数。如果我作为这个模块的维护者,需要没完没了的新增自定义数据类型的重载函数。
函数模板和函数模板实例概念
在进一步了解函数模板前,需要先知道函数模板
和函数模板实例
这个概念。函数模板
概念最开始介绍了,函数模板实例
又是什么?可以类比类的定义和类的实例(对象):
//类的定义
class MyClass
{
}
//类的实例
MyClass a;
//函数模板的定义
template <typename T>
void MyFunction(T a)
{
}
//函数模板的实例(这种称为隐示实例化)
MyFunction<int>(1);
函数模板实例
就是函数模板被指定为具体类型时的代码。 类的定义并不会导致内存的分配,类的实例才会进行内存分配。同样的,函数模板的定义并不会立即触发编译器生成函数,而是在代码中函数模板被指定为具体类型时,编译器就会生成具体的函数。
隐示实例化和显示实例化
//函数模板的定义
template <typename T>
void MyFunction(T a)
{
}
template void MyFunction(int a); //显式实例化,写法一
template void MyFunction<int>(int a); //显式实例化,写法二
int main()
{
MyFunction(1); //隐式实例化,写法一
MyFunction<int>(1); //隐式实例化,写法二
}
- 隐式实例化通过对模板函数的调用,触发函数生成。
- 显式实例化,通过特定的语法规则,提前强制触发函数生成。
他们都可以把
函数模板的具体化
前面函数模板不管是显式实例化还是隐式实例化,都会依照函数模板的函数体来生成函数,生成的这些函数执行逻辑完全是一样的。但是有时候,处理不同类型时,处理逻辑期望不一样,也就是函数体不一样。例如实现加法的函数模板,对于基本类型来说,可以用一样的模板,如果传入的类型是自定义的类呢?这时候就需要函数模板的具体化来实现。
class MyClass
{
public:
MyClass(int x, int y)
{
this->x = x;
this->y = y;
}
int x;
int y;
};
//函数模板
template <typename T>
T Add(T a, T b)
{
return a + b;
}
//函数模板具体化,写法一
template <>
MyClass Add(MyClass a, MyClass b)
{
return MyClass(a.x + b.x, a.y + b.y);
}
//函数模板具体化,写法二
template <>
MyClass Add<MyClass>(MyClass a, MyClass b)
{
return MyClass(a.x + b.x, a.y + b.y);
}
可以看到具体化就是覆盖了原来的模板里面的函数体。和实例化不同的地方除了需要实现自己的函数体外,还有就是template后面多了一对<>
号。实际上上面的例子可以通过对自定义类型操作符重载来实现。上面例子中如果MyClass类里面对+
号操作符重载,同样使用实例化模板函数也可以实现功能。但是如果函数里面的逻辑更复杂,并且实现的逻辑也不是某个操作符的逻辑,那就必须要用函数模板具体化了。
函数模板重载和默认参数,以及默认类型
- 函数模板重载
函数模板也是可以重载的,规则和函数重载一样。例如:
template <typename T>
void foo(T a)
{
}
template <typename T>
void foo(T a , int b)
{
}
foo<int>(1);
foo<int>(1, 2);
- 函数模板中的参数的默认值
template <typename T>
void foo(T a , int b = 0)
{
}
int main()
{
foo<int>(1);
foo<int>(1, 2);
return 0;
}
- 默认类型
template <typename T = char>
void foo(T a)
{
}
int main()
{
foo(1);
foo(0.3);
return 0;
}
函数模板中的默认类型是C++11标准才引入的,之前标准不支持。目前我还没搞清楚这种写法有什么用处。上面2个例子,让编译器自动推导一个被推导为int,一个被推导为double。
函数模板和函数重载
我个人觉得这两者最终都是被统一为函数重载,也就是编译器在编译过程中根据特征标,按照一定的规则,把函数重命名了一下。函数模板无非是多了一个根据指定类型或者自动推导(未显示指定类型时)出的类型,生成函数而已,这个过程中还是有根据特征标重命名函数。
匹配顺序
对于同一个函数名,非模板函数,模板函数是可以同时存在的。函数调用时,会按照匹配度进行调用:
非模板函数 > 实例化或者具体化 > 函数模板
void foo(int) //①
{
}
template <typename T> //②
void foo(T a)
{
}
template void foo(double a); //③
template <>
void foo(short) //④
{
}
int main()
{
int a = 1;
double b = 2.3;
short c = 4;
long d = 4;
foo(a); //调用①
foo(b); //调用③
foo(c); //调用④
foo(d); //调用②
return 0;
}