章节目录

函数模板特化

本节阅读量:

当为给定类型实例化函数模板时,编译器会生成函数副本,并用变量声明中使用的实际类型替换模板类型参数。这意味着每个实例化类型都会得到实现细节相同的函数(只是使用的类型不同)。大多数时候这正是我们想要的,但有时对于特定数据类型,以略有不同的方式实现模板化函数会更有用。


使用非模板函数

考虑以下示例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
#include <iostream>

template <typename T>
void print(const T& t)
{
    std::cout << t << '\n';
}

int main()
{
    print(5);
    print(6.7);
    
    return 0;
}

这会打印:

1
2
5
6.7

现在,假设我们希望以科学记数法输出double值(并且这段逻辑只对double值生效)。

为给定类型获取不同行为的一种方法是定义非模板函数:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
#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&)实例化出来的版本。

这会产生以下结果:

1
2
5
6.700000e+000

以这种方式定义函数的一个好处是,非模板函数不需要与函数模板具有相同的签名。请注意,print(const T&)传递常量引用,而print(double)按值传递。

通常,如果可以使用非模板函数,就优先定义这类函数。


函数模板特化

实现类似结果的另一种方法是使用显式模板特化。显式模板特化允许我们为特定类型或值显式定义模板的不同实现。当所有模板参数都被特化时,称为完全特化。当只有一部分模板参数被特化时,称为部分特化。

当T是double时,我们可以创建一个用于打印的特化函数:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <iostream>

// 这是原始的模板 (必须写在前面)
template <typename T>
void print(const T& t)
{
    std::cout << t << '\n';
}

// template 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&)。

现在,让我们仔细看看我们的函数模板特化:

1
2
template<>                          // 模板参数声明里,没有任何声明的参数
void print<double>(const double& d) // 针对 double 类型特化

首先,我们需要一个模板参数声明,以便编译器知道这里处理的是模板相关内容。然而,在这种情况下,我们实际上不需要任何模板参数,因此使用一对空的尖括号。由于特化中没有模板参数,因此这是一个完全特化。

在下一行,print<double>告诉编译器,我们正在为double类型特化原始的print模板函数。特化必须具有与原始模板相同的函数签名(只是在原始模板使用T的地方替换为double)。由于原始模板具有「const T&」类型的参数,因此特化必须具有「const double&」类型的形参。当原始模板传递引用时,特化不能改为按值传递(反之亦然)。

此示例打印与上面相同的结果。

请注意,如果同时存在匹配的非模板函数和匹配的模板函数特化,非模板函数会被优先使用。此外,完全特化不是隐式内联的,因此如果在头文件中定义它,请确保将其标记为inline,以避免ODR冲突。

与普通函数一样,如果希望解析为特化的任何函数调用产生编译错误,则可以删除函数模板特化(使用=delete)。

通常,您应该尽量避免函数模板特化,改用非模板函数。


成员函数的模板特化?

现在考虑以下类模板:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#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
    Storage i { 5 };
    Storage d { 6.7 };

    // 打印
    i.print();
    d.print();
}

这会打印:

1
2
5
6.7

假设我们再次希望创建一个print()函数版本,用科学记数法打印double。然而,这次print()是成员函数,因此不能定义非成员函数。那么我们该怎么做呢?

尽管看起来这里需要使用函数模板特化,但这并不是合适的工具。请注意,i.print()调用Storage<int>::print(),d.print()调用Storage<double>::print()。因此,如果我们想在T是double时更改此函数的行为,需要特化Storage<double>::print(),这属于类模板特化,而不是函数模板特化!

那么该怎么做呢?我们将在下一课中讨论类模板特化。


26.1 模板非类型参数

上一节

26.3 类模板特化

下一节