Skip to content

19.5 函数模板特化

By Alex on December 3rd, 2016 | last modified by Alex on January 23rd, 2020 | 翻译by dashjay 2020.07.10

当你实例化一个函数模板,使用给定的类型,编译器会复刻一份模板函数,并且用实际定义的要使用的参数类型替换模板类型参数。这意味着一个特定的函数将会在每个实例类有同样的实现细节(也就是说只是类型不同)。大多数情况下,这正是你想要的,偶尔也有一些情况下,在某些特殊类型的数据上实例化模板函数会有轻微的不同。

模板特化就是解决这个的方法之一。

让我们哪来看一个很简单的例子:

template <class T>
class Storage
{
private:
    T m_value;
public:
    Storage(T value)
    {
         m_value = value;
    }

    ~Storage()
    {
    }

    void print()
    {
        std::cout << m_value << '\n';
    }
};

以上的代码可以在很多不同的数据类型下工作。

int main()
{
    // Define some storage units
    Storage<int> nValue(5);
    Storage<double> dValue(6.7);

    // Print out some values
    nValue.print();
    dValue.print();
}

这将产生结果

5
6.7

现在,假设说我们想要 double 类型(仅仅是双精浮点数)输出为特殊的科学计数法。为了达到这个目的,我们可以使用函数模板特化(有时也叫做显式函数模板特化)来为 double 创建一个特殊版本的 print() 函数。这相当简单:简单的定义特化的函数(如果函数是一个类型,在类定义外照做即可。)替换模板类为你想要为之重定义的特殊类型,这就是我们为doubles专门特化后的 print() 函数:

template <>
void Storage<double>::print()
{
    std::cout << std::scientific << m_value << '\n';
}

当编译器实例化 Storage<double>::print() ,他会检查我们早就显式的定义了函数,并且它将会使用那个我们早就定义好的函数替换通用模板函数。

模板 <> 告诉编译器这是一个模板函数,并且没有模板参数(因为在这个例子里,我们已经显式的制定了所有参数)。一些编译器可能允许你省略这个,但是带上它更合适。

结果,我们再次运行之前的的程序,将会打印:

5
6.700000e+000

另一个例子

现在让我们来举另一个例子,这时候的函数特化是可以非常有用。思考如果我们尝试使用char*来实例化我们的模板类 Storage ,将会发生什么?

int main()
{
    // Dynamically allocate a temporary string
    char *string = new char[40];

    // Ask user for their name
    std::cout << "Enter your name: ";
    std::cin >> string;

    // Store the name
    Storage<char*> storage(string);

    // Delete the temporary string
    delete[] string;

    // Print out our value
    storage.print(); // This will print garbage
}

当结果出现时,代替本应该打印用户输入的 storage.print() 打印了垃圾(garbage)!发生了什么?

Storate 使用 char* 实例化时,Storage<char*>的构造函数看起来就像这样:

template <>
Storage<char*>::Storage(char* value)
{
     m_value = value;
}

换句话说,这就是做了一个指针赋值(浅拷贝)!结果,m_valuestring指向相同的内存地址。当我们删除 stringmain 中,我们删除了 m_value 指向的数据!因此,当我们打印这些值时,出现的就是一些垃圾。

幸运的是,我们可以使用模板特化修复这些问题。相比于做一个指针拷贝,我们应该像构造函数那样,对输入的字符串做一个完全拷贝。因此我们来为 char* 这个类写一个特化的构造函数,,专门来解决这个问题。

template <>
Storage<char*>::Storage(char* value)
{
    // Figure out how long the string in value is
    int length=0;
    while (value[length] != '\0')
        ++length;
    ++length; // +1 to account for null terminator

    // Allocate memory to hold the value string
    m_value = new char[length];

    // Copy the actual value string into the m_value memory we just allocated
    for (int count=0; count < length; ++count)
        m_value[count] = value[count];
}

现在当为 Strorage<char*>给变量分配内存时,这个构造函数将会被使用来替代默认的那个。结果 m_value 将会收到它自己的那份 string 的拷贝。因此,当我们删除 string 时,m_value 将不会被影响。

然而,这个类现在有内存泄露当使用 char* 实例化时,因为 m_value 将不会被删除,当一个 Storage 变量离开作用于时。就像你猜的那样,这也可以被解决,通过特化 Storage<char*>

template <>
Storage<char*>::~Storage()
{
    delete[] m_value;
}

现在当 Storate<char*> 离开作用于的时候,特化的构造函数分配的内存将会被特化的析构函数给删除。

以上的例子虽然全都用了成员函数,你仍然可以特化一个非成员模板函数以同样的方式。