当针对某一具体类型实例化函数模板时,编译器会按照模板函数“刻印”出一份副本,并将模板类型参数替换为变量声明中使用的实际类型。这意味着,对于每一个实例化类型,函数的实现细节均保持一致(仅类型不同)。大多数情况下,这正是我们所期望的;然而,偶尔我们也需要针对某一特定数据类型,对模板函数的实现做出细微调整。
使用非模板函数
考虑以下示例:
#include <iostream>
template <typename T>
void print(const T& t)
{
std::cout << t << '\n';
}
int main()
{
print(5);
print(6.7);
return 0;
}
输出为:
5
6.7
现在,假设我们仅希望 double
类型的值以科学计数法输出。
一种实现特定类型不同行为的方式,是定义一个非模板函数:
#include <iostream>
template <typename T>
void print(const T& t)
{
std::cout << t << '\n';
}
void print(double d)
{
std::cout << std::scientific << d << '\n';
}
int main()
{
print(5);
print(6.7);
return 0;
}
当编译器解析 print(6.7)
时,会发现我们已经定义了 print(double)
,于是直接使用该版本,而非从 print(const T&)
实例化。
结果输出为:
5
6.700000e+000
采用这种方式定义函数的一大优点是:非模板函数无需与函数模板签名完全一致。注意 print(const T&)
采用 const
引用传参,而 print(double)
则按值传参。
一般而言,若可通过定义非模板函数实现需求,应优先考虑此方式。
函数模板特化
另一种实现类似效果的方法是使用显式模板特化(explicit template specialization,通常简称模板特化)。该特性允许我们为特定类型或数值显式定义模板的另一种实现。当所有模板参数均被特化时,称为“完全特化”(full specialization);仅部分参数被特化时,则称为“偏特化”(partial specialization)。
下面为 print<T>
创建针对 double
的完全特化:
#include <iostream>
// 主模板(必须首先出现)
template <typename T>
void print(const T& t)
{
std::cout << t << '\n';
}
// 对主模板 print<T> 的 double 完全特化
// 完全特化并非隐式 inline,若置于头文件应加 inline
template<> // 模板参数声明,不含任何模板形参
void print<double>(const double& d) // 针对 double 的特化
{
std::cout << std::scientific << d << '\n';
}
int main()
{
print(5);
print(6.7);
return 0;
}
要对模板进行特化,编译器必须首先看到主模板的声明。上例中主模板为 print<T>(const T&)
。
让我们更仔细地审视该函数模板特化:
template<> // 模板参数声明,不含任何模板形参
void print<double>(const double& d) // 针对 double 的特化
首先,需要模板参数声明,以告知编译器我们正在处理模板相关操作;但由于无需任何模板形参,因而使用一对空的尖括号 <>
。由于特化中无模板形参,故为完全特化。
下一行 print<double>
告知编译器:我们正在为主模板函数 print
提供针对 double
的特化版本。特化必须保持与主模板一致的签名(仅将主模板中的 T
替换为 double
)。由于主模板形参为 const T&
,特化形参必须为 const double&
;特化不能将按引用传参改按值传参(反之亦然)。
此示例输出与先前相同。
注意:若同时存在匹配的非模板函数与匹配的模板函数特化,非模板函数优先被调用。此外,完全特化并非隐式 inline
,若在头文件中定义,应显式标记为 inline
,以避免违反 ODR(一次定义规则)。
警告
完全特化并非隐式 inline
(偏特化隐式为 inline
)。若将完全特化置于头文件,应标记为 inline
,以防止在多个翻译单元包含时出现 ODR 违规。
与普通函数类似,函数模板特化亦可通过 = delete
删除,以使任何匹配该特化的调用产生编译错误。
一般而言,应尽可能避免使用函数模板特化,转而使用非模板函数。
函数模板特化能否用于成员函数?
现在考虑以下类模板:
#include <iostream>
template <typename T>
class Storage
{
private:
T m_value {};
public:
Storage(T value)
: m_value { value }
{
}
void print()
{
std::cout << m_value << '\n';
}
};
int main()
{
// 定义若干存储单元
Storage i { 5 };
Storage d { 6.7 };
// 打印值
i.print();
d.print();
}
输出为:
5
6.7
假设我们再次希望 print()
函数在类型为 double
时以科学计数法输出。然而,此时 print()
是成员函数,我们无法为其定义非成员函数。那么应如何实现?
尽管看似需要使用函数模板特化,但这并非正确工具。注意 i.print()
调用的是 Storage<int>::print()
,而 d.print()
调用的是 Storage<double>::print()
。因此,若想在 T
为 double
时改变此函数行为,我们需要特化的是 Storage<double>::print()
,这属于类模板特化,而非函数模板特化。
那么具体应如何操作?我们将在下一课程中介绍类模板特化。