函数模板特化

当针对某一具体类型实例化函数模板时,编译器会按照模板函数“刻印”出一份副本,并将模板类型参数替换为变量声明中使用的实际类型。这意味着,对于每一个实例化类型,函数的实现细节均保持一致(仅类型不同)。大多数情况下,这正是我们所期望的;然而,偶尔我们也需要针对某一特定数据类型,对模板函数的实现做出细微调整。

使用非模板函数

考虑以下示例:

#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()。因此,若想在 Tdouble 时改变此函数行为,我们需要特化的是 Storage<double>::print(),这属于类模板特化,而非函数模板特化。

那么具体应如何操作?我们将在下一课程中介绍类模板特化。

关注公众号,回复"cpp-tutorial"

可领取价值199元的C++学习资料

公众号二维码

扫描上方二维码或搜索"cpp-tutorial"