类模板特化

在课程函数模板特化中,我们了解到可以对函数进行特化,以便为特定数据类型提供不同的功能。事实上,不仅函数可以特化,类也可以特化!

类模板特化简介

假设我们需要一个能够存储 8 个对象的类。下面是一个简化的类模板示例:

#include <iostream>

template <typename 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];
    }
};

int main()
{
    // 定义一个存储 int 的 Storage8
    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';

    // 定义一个存储 bool 的 Storage8
    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> 编写定制版本,以取代通用的 Storage8<T>

类模板特化被视作完全独立的类,尽管其实例化方式与原模板类相同。这意味着我们可以对特化类进行任意修改,包括实现细节乃至公开接口,就如同它是一个独立类。

与其他模板一样,编译器必须能看到特化的完整定义才能使用。此外,定义类模板特化前,必须先定义非特化版本。

下面为 Storage8<bool> 的特化示例:

#include <cstdint>

// 首先定义非特化的类模板
template <typename 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];
    }
};

// 现在定义特化的类模板
template <> // 表明这是一个无模板参数的模板类
class Storage8<bool> // 为 bool 特化 Storage8
{
// 以下为标准类的实现细节

private:
    std::uint8_t m_data{};

public:
    // 以下函数实现细节无需关注
    void set(int index, bool value)
    {
        // 计算要设置/清除的位
        // 生成一个仅在目标位为 1 的掩码
        auto mask{ 1 << index };

        if (value)          // 若要置位
            m_data |= mask; // 使用按位或置位
        else                // 若要清位
            m_data &= ~mask; // 使用按位与逆掩码清位
    }

    bool get(int index)
    {
        // 计算要获取的位
        auto mask{ 1 << index };
        // 按位与获取目标位,然后隐式转换为 bool
        return (m_data & mask);
    }
};

// 与之前相同的示例
int main()
{
    // 定义一个存储 int 的 Storage8(实例化 Storage8<T>,其中 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';
    }

    // 定义一个存储 bool 的 Storage8(实例化 Storage8<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;
}

首先注意,特化类模板以 template<> 开头。template 关键字告知编译器后续为模板相关定义,空尖括号表示无模板形参,因为我们将唯一模板形参 T 替换为具体类型 bool

接着,在类名 Storage8 后添加 <bool>,表示这是 Storage8bool 特化版本。

其余修改均为实现细节。你无需理解位运算细节即可使用该类(如需复习位运算,可回顾课程位运算符)。

该特化类使用 std::uint8_t(1 字节无符号整数)代替 8 个 bool 组成的数组(8 字节)。

当实例化 Storage8<T>T 不为 bool 时,编译器将使用通用模板 Storage8<T>;当实例化 Storage8<bool> 时,将使用我们刚创建的特化版本。注意,我们保持了两个类的公共接口一致——尽管 C++ 允许我们按需增删改 Storage8<bool> 的成员,但保持接口一致意味着程序员可以以完全相同的方式使用任一类。

如你所料,输出结果与之前使用非特化 Storage8<bool> 的版本相同:

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

成员函数特化

在课程中,我们给出以下示例:

#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();
}

我们希望特化 print() 函数,使其在类型为 double 时以科学计数法输出。使用类模板特化,我们可以为 Storage<double> 定义特化:

#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';
    }
};

// Storage<double> 的显式类模板特化
// 注意其冗余性
template <>
class Storage<double>
{
private:
    double m_value {};
public:
    Storage(double value)
      : m_value { value }
    {
    }

    void print();
};

// 为演示原因,我们在类外定义该函数
// 这是一个普通(非特化)成员函数定义(特化类 Storage<double> 的成员函数 print)
void Storage<double>::print()
{
    std::cout << std::scientific << m_value << '\n';
}

int main()
{
    // 定义若干存储单元
    Storage i { 5 };
    Storage d { 6.7 }; // 使用显式特化 Storage<double>

    // 打印值
    i.print(); // 调用 Storage<int>::print(由 Storage<T> 实例化)
    d.print(); // 调用 Storage<double>::print(来自 Storage<double> 的显式特化)
}

然而,上述方法冗余极大:我们复制了整个类定义,只为修改一个成员函数!

幸运的是,我们有更简洁的方案。C++ 并不要求显式特化 Storage<double> 才能特化 Storage<double>::print()。相反,我们可以让编译器从 Storage<T> 隐式实例化 Storage<double>,并仅显式特化 Storage<double>::print()!示例如下:

#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';
    }
};

// 这是成员函数的显式特化定义
// 显式函数特化并非隐式 inline,若置于头文件请加 inline
template<>
void Storage<double>::print()
{
    std::cout << std::scientific << m_value << '\n';
}

int main()
{
    // 定义若干存储单元
    Storage i { 5 };
    Storage d { 6.7 }; // 将隐式实例化 Storage<double>

    // 打印值
    i.print(); // 调用 Storage<int>::print(由 Storage<T> 实例化)
    d.print(); // 调用 Storage<double>::print(来自 Storage<double>::print() 的显式特化)
}

完成!

如在课程函数模板特化中,显式函数特化并非隐式 inline,因此若在头文件中定义 Storage<double>::print() 的特化,应标记为 inline

类模板特化的定义位置

为了使用特化,编译器必须同时看到非特化类与特化类的完整定义。若编译器只能看到非特化类的定义,则会使用非特化版本。

因此,特化类与特化函数通常定义在头文件中非特化类的定义之后,从而只需包含一个头文件即可获得非特化类及其所有特化。这确保只要能看到非特化类,就能看到对应的特化。

若特化仅在某单一翻译单元中使用,可定义于该翻译单元的源文件中。由于其他翻译单元无法看到该特化定义,它们将继续使用非特化版本。

应避免将特化单独置于一个头文件中,并仅在需要特化的翻译单元包含该头文件。依赖头文件是否存在以透明改变行为的设计并不可取。例如,若你意图使用特化却忘记包含特化头文件,可能会误用非特化版本;反之,若你意图使用非特化版本,却因其他头文件间接包含了特化头文件而意外使用了特化。

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

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

公众号二维码

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