函数模板

函数模板是提供生成函数能力的语法。换而言之,函数模板提供了一簇函数供编译器选择。

函数模板语法

函数模板拥有这样的语法:

template<模板形参列表>
(允许出现模板形参的)函数声明(可为定义)

注意

template<模板形参列表> 是函数模板语法的一部分,其后没有分号;模板形参列表与函数声明分两行写是约定俗成的编码习惯。

模板形参列表 是由一系列逗号分隔的 模板形参。函数模板可以生成一系列形如 函数声明 的函数,并替换 函数声明 中出现的 模板形参

比如上一节中的例子:

template<typename T>
void print(T x) { /* 定义略 */ }

这里,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,并将它用在函数声明的参数中。

typenameclass 都可用于引入类型模板形参,它们完全等价;但更推荐使用 typename

模板实参

模板实参是模板实例化的依据。最简单地,可以通过“带模板实参”的函数模板调用表达式来实例化函数模板。

所谓“带模板实参”的调用表达式是这样的:

int main() {
    print<int>(42); // 以 int 作为 print 模板的模板实参
}

这里,模板实参列表是在函数模板名后添加以尖括号围起的若干个类型名。这里,int 就是一个模板实参。

当代码中出现了带模板实参的函数模板调用,那么就开始进行一次模板实例化。具体而言,模板实例化会取出模板的声明,然后用模板实参列表中的实参替换声明中出现的模板形参。替换完成后,整个声明就成为了模板的一份实例化函数。最终被调用的函数也正是这个实例化出来的函数。

举一个更复杂的模板实例化的例子。如下模板和调用表达式

#include <iostream>
template<typename T, typename U>
void add(T a, U b) {
    T c{a + b};
    std::cout << c << std::endl;
}
int main() {
    add<float, int>(1, 2);
}

模板实参列表为 float, int,形参列表为 typename T, typename U。此时,实例化将以 float 替换 add 模板中的 T,以 int 替换 add 模板中的 U。从而得到下面的实例化结果:

void add(float a, int b) {
    float c{a + b};
    std::cout << c << std::endl;
}

然后 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。这就完成了模板实参的推导。

函数模板实参推导通过一套极其复杂的流程open in new window实现。我们并不深究其原理,它在日常的使用中一般不会遇到麻烦。如果遇到了麻烦,我们也只需再次显式指定模板实参即可。

严格讲,模板实参和形参的结合应称为特化(specialization)而非实例化,实例化是使用模板的特化后才会发生的。但为了避免与后续的模板显式特化语法混淆,这里我只用实例化来代指整个过程。

函数模板实例化(含 TAD)之后会进行重载决议(Overload resolution)来选择合适的重载进行调用。重载决议是比 TAD 还复杂的机制,几乎不可能用一言两语解释清楚。

最近更新:
代码未运行