函数模板
函数模板是提供生成函数能力的语法。换而言之,函数模板提供了一簇函数供编译器选择。
函数模板语法
函数模板拥有这样的语法:
template<模板形参列表> (允许出现模板形参的)函数声明(可为定义)
注意
template<模板形参列表>
是函数模板语法的一部分,其后没有分号;模板形参列表与函数声明分两行写是约定俗成的编码习惯。
模板形参列表
是由一系列逗号分隔的 模板形参
。函数模板可以生成一系列形如 函数声明
的函数,并替换 函数声明
中出现的 模板形参
。
比如上一节中的例子:
这里,typename T
就是模板形参,void print(T x) ...
就是函数声明。这个模板可以生成若干个 T
不同的 print
函数。
函数模板的实例化
从函数模板生成函数的过程称为这个函数模板的实例化(Instantiation)。比如
template<typename T>
void print(T x) { /* 定义略 */ }
int main() {
print(42); // 导致了 print 模板实例化出 void print(int x);
print(3.14); // 导致了 print 模板实例化出 void print(double x);
}
这个例子中,模板 print
实例化为两个函数 void print(int);
和 void print(double);
。接下来的篇幅我将重点描述模板是如何实例化的。
模板形参
模板实例化的基本原理是“替换”,而模板形参指示了模板声明中可被替换的部分。最常用的模板形参是类型模板形参(Type template parameter)。类型模板形参拥有这样的语法:
typenameclass 形参名
类型模板形参引入若干个 形参名
之后,后续的函数声明中就如同定义了 形参名
这个类型。比如模板 print
中就可以使用类型模板形参 T
,并将它用在函数声明的参数中。
typename
和class
都可用于引入类型模板形参,它们完全等价;但更推荐使用typename
。
模板实参
模板实参是模板实例化的依据。最简单地,可以通过“带模板实参”的函数模板调用表达式来实例化函数模板。
所谓“带模板实参”的调用表达式是这样的:
这里,模板实参列表是在函数模板名后添加以尖括号围起的若干个类型名。这里,int
就是一个模板实参。
当代码中出现了带模板实参的函数模板调用,那么就开始进行一次模板实例化。具体而言,模板实例化会取出模板的声明,然后用模板实参列表中的实参替换声明中出现的模板形参。替换完成后,整个声明就成为了模板的一份实例化函数。最终被调用的函数也正是这个实例化出来的函数。
举一个更复杂的模板实例化的例子。如下模板和调用表达式
模板实参列表为 float, int
,形参列表为 typename T, typename U
。此时,实例化将以 float
替换 add
模板中的 T
,以 int
替换 add
模板中的 U
。从而得到下面的实例化结果:
然后 add<float, int>(1, 2)
就会调用这个 void add(float, int)
函数。如果实例化的结果是非法的(比如不存在对应的运算符重载、成员函数等),那么编译器会给出错误。
对于每一次不同的带模板实参的函数调用,都会实例化出不同的函数。如果出现多次带相同的模板实参的函数调用,则只会进行一次对应的实例化。
int main() {
// print<int> 和 print<char> 是两个不同的函数,尽管它们非常相似
print<int>(42);
print<char>('H');
print<int>(56); // 不会再实例化出另外一个 print<int>
}
形如
模板名<形参列表>
的语法称为模板标识(Template ID)。当编译通过时,模板标识总是指代一个模板的实例。
函数模板实参推导
如果每次实例化都需指明模板实参,那么代码编写起来未免有些费劲。因此 C++ 提供了函数模板实参推导(Function Template Argument Deduction,TAD)这一机制。
所谓函数模板实参推导,就是根据函数实参来推导模板实参的值。举一个简单的例子,对于函数模板调用表达式
int main() {
print(42);
}
中,42
具有 int
类型,那么这里期望一个 void print(int);
类型的实例。因此,编译器有能力推导出模板声明 void print(T);
中的形参 T
应该取值为 int
。这就完成了模板实参的推导。
函数模板实参推导通过一套极其复杂的流程实现。我们并不深究其原理,它在日常的使用中一般不会遇到麻烦。如果遇到了麻烦,我们也只需再次显式指定模板实参即可。
严格讲,模板实参和形参的结合应称为特化(specialization)而非实例化,实例化是使用模板的特化后才会发生的。但为了避免与后续的模板显式特化语法混淆,这里我只用实例化来代指整个过程。
函数模板实例化(含 TAD)之后会进行重载决议(Overload resolution)来选择合适的重载进行调用。重载决议是比 TAD 还复杂的机制,几乎不可能用一言两语解释清楚。