函数模板特化
本节阅读量:当为给定类型实例化函数模板时,编译器会生成函数副本,并用变量声明中使用的实际类型替换模板类型参数。这意味着每个实例化类型都会得到实现细节相同的函数(只是使用的类型不同)。大多数时候这正是我们想要的,但有时对于特定数据类型,以略有不同的方式实现模板化函数会更有用。
使用非模板函数
考虑以下示例:
|
|
这会打印:
|
|
现在,假设我们希望以科学记数法输出double值(并且这段逻辑只对double值生效)。
为给定类型获取不同行为的一种方法是定义非模板函数:
|
|
当编译器解析print(6.7)时,会发现我们已经定义了print(double),于是使用它,而不是使用从print(const T&)实例化出来的版本。
这会产生以下结果:
|
|
以这种方式定义函数的一个好处是,非模板函数不需要与函数模板具有相同的签名。请注意,print(const T&)传递常量引用,而print(double)按值传递。
通常,如果可以使用非模板函数,就优先定义这类函数。
函数模板特化
实现类似结果的另一种方法是使用显式模板特化。显式模板特化允许我们为特定类型或值显式定义模板的不同实现。当所有模板参数都被特化时,称为完全特化。当只有一部分模板参数被特化时,称为部分特化。
当T是double时,我们可以创建一个用于打印的特化函数:
|
|
为了特化模板,编译器必须先看到原始模板声明。上例中的原始模板是print<T>(const T&)。
现在,让我们仔细看看我们的函数模板特化:
|
|
首先,我们需要一个模板参数声明,以便编译器知道这里处理的是模板相关内容。然而,在这种情况下,我们实际上不需要任何模板参数,因此使用一对空的尖括号。由于特化中没有模板参数,因此这是一个完全特化。
在下一行,print<double>告诉编译器,我们正在为double类型特化原始的print模板函数。特化必须具有与原始模板相同的函数签名(只是在原始模板使用T的地方替换为double)。由于原始模板具有「const T&」类型的参数,因此特化必须具有「const double&」类型的形参。当原始模板传递引用时,特化不能改为按值传递(反之亦然)。
此示例打印与上面相同的结果。
请注意,如果同时存在匹配的非模板函数和匹配的模板函数特化,非模板函数会被优先使用。此外,完全特化不是隐式内联的,因此如果在头文件中定义它,请确保将其标记为inline,以避免ODR冲突。
与普通函数一样,如果希望解析为特化的任何函数调用产生编译错误,则可以删除函数模板特化(使用=delete)。
通常,您应该尽量避免函数模板特化,改用非模板函数。
警告
完全特化不是隐式内联的(部分特化是隐式内联的)。如果在头文件中放置完全特化,则应将其标记为inline,避免它被包含到多个翻译单元中时导致ODR冲突。
成员函数的模板特化?
现在考虑以下类模板:
|
|
这会打印:
|
|
假设我们再次希望创建一个print()函数版本,用科学记数法打印double。然而,这次print()是成员函数,因此不能定义非成员函数。那么我们该怎么做呢?
尽管看起来这里需要使用函数模板特化,但这并不是合适的工具。请注意,i.print()调用Storage<int>::print(),d.print()调用Storage<double>::print()。因此,如果我们想在T是double时更改此函数的行为,需要特化Storage<double>::print(),这属于类模板特化,而不是函数模板特化!
那么该怎么做呢?我们将在下一课中讨论类模板特化。