在课程函数模板特化中,我们了解到可以对函数进行特化,以便为特定数据类型提供不同的功能。事实上,不仅函数可以特化,类也可以特化!
类模板特化简介
假设我们需要一个能够存储 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>
,表示这是 Storage8
的 bool
特化版本。
其余修改均为实现细节。你无需理解位运算细节即可使用该类(如需复习位运算,可回顾课程位运算符)。
该特化类使用 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
。
类模板特化的定义位置
为了使用特化,编译器必须同时看到非特化类与特化类的完整定义。若编译器只能看到非特化类的定义,则会使用非特化版本。
因此,特化类与特化函数通常定义在头文件中非特化类的定义之后,从而只需包含一个头文件即可获得非特化类及其所有特化。这确保只要能看到非特化类,就能看到对应的特化。
若特化仅在某单一翻译单元中使用,可定义于该翻译单元的源文件中。由于其他翻译单元无法看到该特化定义,它们将继续使用非特化版本。
应避免将特化单独置于一个头文件中,并仅在需要特化的翻译单元包含该头文件。依赖头文件是否存在以透明改变行为的设计并不可取。例如,若你意图使用特化却忘记包含特化头文件,可能会误用非特化版本;反之,若你意图使用非特化版本,却因其他头文件间接包含了特化头文件而意外使用了特化。