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)

虽然这个类功能齐全,但其实现Storage8T 的变量bool最终占用了一个完整的字节,即使理论上它只需要一个比特来存储其真或假值!因此,一个 T变量bool占用了 1 个比特的有效信息和 7 个比特的空间。我们的Storage8T 变量bool,总共占用了 1 个字节的有效信息和 7 个字节的空间。
事实证明,利用一些基本的位逻辑,可以将所有 8 个布尔值压缩到一个字节中,从而完全消除空间浪费。然而,为了实现这一点,我们需要修改与 bool 类型一起使用时的类bool,将包含 8 个布尔值的数组替换bool为一个单字节大小的变量。虽然我们可以为此创建一个全新的类,但这有一个主要的缺点:我们必须给它起一个不同的名字。这样,程序员就必须记住 bool 类Storage8bool 类Storage8Bool(或者我们给新类起的任何名字)用于 bool 类型bool。这种不必要的复杂性我们宁愿避免。幸运的是,C++ 为我们提供了一种更好的方法:类模板特化。
类模板特化
类模板特化允许我们针对特定数据类型(或多个模板参数时对应的数据类型)对模板类进行特化。在本例中,我们将使用类模板特化来编写一个自定义版本,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)。
其他所有更改都只是类实现细节。您无需理解位逻辑的工作原理即可使用该类(当然,如果您想弄明白,但需要复习一下位运算符的工作原理,可以查看O.2——位运算符)。
请注意,此特殊类使用std::uint8_t1 字节无符号整数,而不是 8 字节数组bool。
现在,当我们实例化一个对象类型 T StorageT 不是 A)bool,我们会得到一个基于泛型模板Storage8T 类型的对象时Storage8T 的函数Storage8
正如你所预料的,这会输出与之前使用非特殊版本示例相同的结果Storage8

特化成员函数
在上一课中,我们介绍了以下示例:
#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();
}

我们希望对该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>)
}

但是,请注意这里存在多少冗余。我们仅仅为了修改一个成员函数,就复制了整个类定义!
幸运的是,我们可以做得更好。C++ 并不要求我们显式地特化 AStorageB Storage A 特化StorageB 的显式特化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';
}
};
// 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())
}

就是这样!
如前一课(26.3——函数模板特化)中所述,显式函数特化不会隐式内联,因此,Storage
在哪里定义类模板特化
为了使用特化,编译器必须能够看到非特化类和特化类的完整定义。如果编译器只能看到非特化类的定义,它将使用非特化类而不是特化类。
因此,特化类和函数通常定义在紧邻普通类定义的头文件中,这样,包含单个头文件即可同时包含普通类及其所有特化版本。这确保了只要能看到普通类,就能看到特化版本。
如果某个特化仅在单个翻译单元中需要,则可以在该翻译单元的源文件中定义。由于其他翻译单元无法看到该特化定义,它们将继续使用非特化版本。
要谨慎对待将特化功能放在单独的头文件中的做法,并试图在任何需要使用该特化的翻译单元中包含该特化功能的头文件。设计代码时,如果代码的行为会根据头文件的存在与否而透明地改变,这是一种糟糕的做法。例如,如果您打算使用特化功能,但忘记包含该特化功能的头文件,最终可能会使用非特化版本。如果您打算使用非特化版本,但其他头文件通过传递包含的方式包含了该特化功能,那么您最终可能仍然会使用特化版本。

浙公网安备 33010602011771号