Skip to content

19.6 类模板特化

By Alex on August 16th, 2008 | last modified by Alex on December 21st, 2020

翻译 by dashjay 2020-12-27 |最后修改于 2020-12-27

在之前的课程 19.5 -- Function template specialization,我们知道了可以特化函数,为了给特殊的数据类型提供不同的功能。不仅仅可以特化函数,也可以特化整个类!

思考这个例子,你想设计一个类可以存储 8 个对象。这有一个简化类这样做:

template <class T>
class Storage8
{
private:
    T m_array[8];

public:
    void set(int index, const T &value)
    {
        m_array[index] = value;
    }

    const T& get(int index) const
    {
        return m_array[index];
    }
};

因为这个类是模板类,它能在任何给定的数据类型下工作:

#include <iostream>

int main()
{
    // Define a Storage8 for integers
    Storage8<int> intStorage;

    for (int count{ 0 }; count < 8; ++count)
        intStorage.set(count, count);

    for (int count{ 0 }; count < 8; ++count)
        std::cout << intStorage.get(count) << '\n';

    // Define a Storage8 for bool
    Storage8<bool> boolStorage;
    for (int count{ 0 }; count < 8; ++count)
        boolStorage.set(count, count & 3);

    std::cout << std::boolalpha;

    for (int count{ 0 }; count<8; ++count)
    {
        std::cout << boolStorage.get(count) << '\n';
    }

    return 0;
}

例子打印:

0
1
2
3
4
5
6
7
false
true
true
true
false
true
true
true

这个类是可以完全正常工作的,但是就是 Storage8<bool> 的实现效率远远低于实际需要。因为所有的变量必须有一个地址,并且 CPU 不能定位比一 byte 更小的单位,所有变量必须至少是 1 byte。因此,一个 bool 值整整用掉了整个 byte,然后 7 bits 都被浪费了。我们的 Storage8<bool> 类一共包含了 8 个bool值,其中 1 byte 是包含有用的信息,另外 7 byte 都是浪费的。

结果,使用一个基础位逻辑,有可能吧 8 个 bool 值压缩到一个单 byte 中,消除浪费的空间到一起。然而,为了达成这个目的,我们需要在使用 bool 的时候,修改这个类型,替换 8 个 bool 的数组为一个单 byte 大小的变量。我们当然可以创建整个的新类来完成需求,但是这样做有一个最大的缺点就是:我们不得不取一个新的名字。程序员就必须记在脑子里, Storage8<T> 不能提供给 bool 类使用,必须使用 Storage8Bool (或者什么其他的新类)。这些没有必要的复杂我们应该避免。幸运的是,C++ 给我们提供了一种更好的方法:类模板特化。

类模板特化

类模板特化允许我们特化模板在某个特定类型(或者几个不同的数据类型,如果不止一个模板参数的话)。在这个情况下,我们要去使用模板类特化来写一个特定版本的 Storage8<bool> 将会比泛型的 Storage8<T> 类优先级更高。这个类似于特定函数优先于泛型模板函数。

尽管他们和模板类一样,以同样的方式分配,类模板特化被当做是独立的类。这意味着我们可以改变关于我们特化模板的任何内容,包含它实现·的方式,甚至是公开的函数,就像他是一个独立的类,这是我们的特化类:

template <> // the following is a template class with no templated parameters
class Storage8<bool> // we're specializing Storage8 for bool
{
// What follows is just standard class implementation details
private:
    unsigned char m_data{};

public:
    void set(int index, bool value)
    {
        // Figure out which bit we're setting/unsetting
        // This will put a 1 in the bit we're interested in turning on/off
        auto mask{ 1 << index };

        if (value)  // If we're setting a bit
            m_data |= mask;  // Use bitwise-or to turn that bit on
        else  // if we're turning a bit off
            m_data &= ~mask;  // bitwise-and the inverse mask to turn that bit off
 }

    bool get(int index)
    {
        // Figure out which bit we're getting
        auto mask{ 1 << index };
        // bitwise-and to get the value of the bit we're interested in
        // Then implicit cast to boolean
        return (m_data & mask);
    }
};

首先,注意我们使用 template<> 来开始。模板关键词告诉编译器下方的内容是属于模板的,空的尖括号表示没有任何模板参数。在这种情况下,无需任何的模板参数,因为我们替换仅有的模板参数(T)为特定的类型(bool)。

下一步,我们添加了 <bool> 到类名来表示我们特化了 bool 版本的 Storage8 类。

所有的其他的改变就只是类实现的细节。仅仅为了使用这个类,你完全没有必要理解位逻辑(尽管你可以复习 O.2 -- Bitwise operators 如果你想要解决,但是需要复习如何进行未操作)。

注意这个特化类利用了一个 single unsigned char (1 byte) 替换了一个 8 个 bool 的数组(8 bytes)。

现在,当我们生命一个类型为 Storage8<T> ,并且 T 不是一个 bool 类型,我们就会获得一个从通用模板类 Storage8<T> 生成的版本。

现在,当我们声明了一个 Storage8<T> 的类,当 T 不是一个 bool ,我们将会得到一个从通用泛型版本模板 Storage8<T> 的版本。当我们定义一个 Stroage8<bool> 的对象,我们会得到刚刚创建的特化的版本。注意,我们保持了两个类的公开接口相同 ———— C++ 给我们自由来添加,移除,或者将 Storage8<bool> 的函数改成我们看到的那样,来适配 T 所制定的类型,保持了一个统一的接口是的程序员可以以同样的方式来使用这个类。

我们可以使用和之前恰好相同的方式来展示 Storage8<T>Storage8<bool> 被实例化:

int main()
{
    // Define a Storage8 for integers (instantiates Storage8<T>, where T = int)
    Storage8<int> intStorage;

    for (int count{ 0 }; count < 8; ++count)
    {
        intStorage.set(count, count);
    }

    for (int count{ 0 }; count<8; ++count)
    {
        std::cout << intStorage.get(count) << '\n';
    }

    // Define a Storage8 for bool  (instantiates Storage8<bool> specialization)
    Storage8<bool> boolStorage;

    for (int count{ 0 }; count < 8; ++count)
    {
        boolStorage.set(count, count & 3);
    }

    std::cout << std::boolalpha;

    for (int count{ 0 }; count < 8; ++count)
    {
        std::cout << boolStorage.get(count) << '\n';
    }

    return 0;
}

如你所期待的那样,这打印出了和之前没有进行模板特化的时候的 Storage8<bool> 产生相同的结果。

0
1
2
3
4
5
6
7
false
true
true
true
false
true
true
true

值得再次注意的是,保持模板类和模板特化的公开接口相同是一个不错的习惯,因为它让这些代码更易用 ———— 然而,也不是严格有必要这样做的。