26-4 类模板特化

在上一课26.3——函数模板特化中,我们学习了如何对函数进行特化,以便为特定数据类型提供不同的功能。事实证明,不仅可以对函数进行特化,还可以对类进行特化!

假设你需要一个能够存储 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()
{
    // 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;
}

此示例输出:(output format change)

img

虽然这个类功能齐全,但其实现Storage8效率却远低于预期。由于所有变量都必须有地址,而 CPU 的寻址范围最小为一个字节,因此所有变量的大小至少为一个字节。结果,一个类型为 T 的变量bool最终占用了一个完整的字节,即使理论上它只需要一个比特来存储其真或假值!因此,一个 T变量bool占用了 1 个比特的有效信息和 7 个比特的空间。我们的Storage8类包含 8 个 T 变量bool,总共占用了 1 个字节的有效信息和 7 个字节的空间。

事实证明,利用一些基本的位逻辑,可以将所有 8 个布尔值压缩到一个字节中,从而完全消除空间浪费。然而,为了实现这一点,我们需要修改与 bool 类型一起使用时的类bool,将包含 8 个布尔值的数组替换bool为一个单字节大小的变量。虽然我们可以为此创建一个全新的类,但这有一个主要的缺点:我们必须给它起一个不同的名字。这样,程序员就必须记住 bool 类Storage8用于非布尔类型,而bool 类Storage8Bool(或者我们给新类起的任何名字)用于 bool 类型bool。这种不必要的复杂性我们宁愿避免。幸运的是,C++ 为我们提供了一种更好的方法:类模板特化。

类模板特化

类模板特化允许我们针对特定数据类型(或多个模板参数时对应的数据类型)对模板类进行特化。在本例中,我们将使用类模板特化来编写一个自定义版本,Storage8该版本将优先于通用Storage8类。

类模板特化被视为完全独立的类,即使它们的实例化方式与模板类相同。这意味着我们可以像对待独立类一样,对特化类进行任何更改,包括其实现方式,甚至包括其公开的函数。

与其他模板一样,编译器必须能够看到特化的完整定义才能使用它。此外,定义类模板特化需要先定义未特化的类。

以下是一个特殊Storage8班级的例子:

#include <cstdint>

// First define our non-specialized class template
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];
    }
};

// Now define our specialized class template
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:
    std::uint8_t m_data{};

public:
    // Don't worry about the details of the implementation of these functions
    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);
    }
};

// Same example as before
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;
}

首先,请注意我们特化的类模板以 .template 开头template<>。template 关键字告诉编译器后面是一个模板,而空的尖括号表示没有任何模板参数。在本例中,没有模板参数是因为我们将唯一的模板参数(<T> T)替换为一个特定类型(<T> bool)。

接下来,我们在类名中添加,以表示我们正在专门化一个bool版本class Storage8。

其他所有更改都只是类实现细节。您无需理解位逻辑的工作原理即可使用该类(当然,如果您想弄明白,但需要复习一下位运算符的工作原理,可以查看O.2——位运算符)。

请注意,此特殊类使用std::uint8_t1 字节无符号整数,而不是 8 字节数组bool。

现在,当我们实例化一个对象类型 T Storage(其中TT 不是 A)bool,我们会得到一个基于泛型模板Storage8类的模板版本。当我们实例化一个 T 类型的对象时Storage8,我们会得到我们刚刚创建的专用版本。请注意,我们保持了这两个类的公共接口一致——虽然 C++ 允许我们根据需要自由地添加、删除或更改 T 的函数Storage8,但保持一致的接口意味着程序员可以以完全相同的方式使用这两个类。

正如你所预料的,这会输出与之前使用非特殊版本示例相同的结果Storage8:(output format be changed)

img

特化成员函数

在上一课中,我们介绍了以下示例:

#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()
{
    // Define some storage units
    Storage i { 5 };
    Storage d { 6.7 };

    // Print out some values
    i.print();
    d.print();
}

img

我们希望对该print()函数进行特殊处理,使其能够以科学计数法打印双精度浮点数。利用类模板特化,我们可以定义一个专门的类Storage

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

// Explicit class template specialization for Storage<double>
// Note how redundant this is
template <>
class Storage<double>
{
private:
    double m_value {};
public:
    Storage(double value)
      : m_value { value }
    {
    }

    void print();
};

// We're going to define this outside the class for reasons that will become obvious shortly
// This is a normal (non-specialized) member function definition (for member function print of specialized class Storage<double>)
void Storage<double>::print()
{
    std::cout << std::scientific << m_value << '\n';
}

int main()
{
    // Define some storage units
    Storage i { 5 };
    Storage d { 6.7 }; // uses explicit specialization Storage<double>

    // Print out some values
    i.print(); // calls Storage<int>::print (instantiated from Storage<T>)
    d.print(); // calls Storage<double>::print (called from explicit specialization of Storage<double>)
}

img

但是,请注意这里存在多少冗余。我们仅仅为了修改一个成员函数,就复制了整个类定义!

幸运的是,我们可以做得更好。C++ 并不要求我们显式地特化 AStorage来显式地特化 B Storage::print()。相反,我们可以让编译器隐式地Storage A 特化Storage,然后只提供 B 的显式特化Storage::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';
    }
};

// This is a specialized member function definition
// Explicit function specializations are not implicitly inline, so make this inline if put in header file
template<>
void Storage<double>::print()
{
    std::cout << std::scientific << m_value << '\n';
}

int main()
{
    // Define some storage units
    Storage i { 5 };
    Storage d { 6.7 }; // will cause Storage<double> to be implicitly instantiated

    // Print out some values
    i.print(); // calls Storage<int>::print (instantiated from Storage<T>)
    d.print(); // calls Storage<double>::print (called from explicit specialization of Storage<double>::print())
}

img

就是这样!

如前一课(26.3——函数模板特化)中所述,显式函数特化不会隐式内联,因此,Storage::print()如果函数特化是在头文件中定义的,我们应该将其标记为内联。

在哪里定义类模板特化

为了使用特化,编译器必须能够看到非特化类和特化类的完整定义。如果编译器只能看到非特化类的定义,它将使用非特化类而不是特化类。

因此,特化类和函数通常定义在紧邻普通类定义的头文件中,这样,包含单个头文件即可同时包含普通类及其所有特化版本。这确保了只要能看到普通类,就能看到特化版本。

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

要谨慎对待将特化功能放在单独的头文件中的做法,并试图在任何需要使用该特化的翻译单元中包含该特化功能的头文件。设计代码时,如果代码的行为会根据头文件的存在与否而透明地改变,这是一种糟糕的做法。例如,如果您打算使用特化功能,但忘记包含该特化功能的头文件,最终可能会使用非特化版本。如果您打算使用非特化版本,但其他头文件通过传递包含的方式包含了该特化功能,那么您最终可能仍然会使用特化版本。

posted @ 2025-11-29 17:06  游翔  阅读(16)  评论(0)    收藏  举报