C++学习笔记----11、模块、头文件及各种主题(一)---- 模板概览与类模板(7)
2.6、类模板特殊化
对于特定类型可以提供另外的类模板的实现。例如,你可能决定Grid的const char*(C风格的字符串)的行为是讲不通的。Grid<const char*>会保存其元素在vector<optional<const char*>>中。拷贝构造函数与赋值操作符会执行该const char*指针的空拷贝。对于const char*,进行字符串的深度拷贝是讲得通的。最简单的解决方案是特别为const char*写另外一个实现,它将char *转变为C++ string并且保存在vector<optional<string>>中。
另外一种模板的实现叫模板特殊化。一开始可能会发现语法有一点儿怪怪的。当写一个类模板特殊化时,必须指定它是基于模板的,并且是在写一个特定类型的一个模板的版本。下面是Grid的const char*特殊化的语法。在这个实现中,原来的Grid类模板被移到了一个叫做main的模块接口分区中,而特殊化在一个叫做string的模块接口分区中。
export module grid:string;
import std;
// When the template specialization is used, the original template must be
// visible too.
import :main;
export
template <>
class Grid<const char*>
{
public:
explicit Grid(std::size_t width = DefaultWidth, std::size_t height = DefaultHeight);
virtual ~Grid() = default;
// Explicitly default a copy constructor and copy assignment operator.
Grid(const Grid& src) = default;
Grid& operator=(const Grid& rhs) = default;
// Explicitly default a move constructor and move assignment operator.
Grid(Grid&& src) = default;
Grid& operator=(Grid&& rhs) = default;
std::optional<std::string>& at(std::size_t x, std::size_t y);
const std::optional<std::string>& at(std::size_t x, std::size_t y) const;
std::size_t getHeight() const { return m_height; }
std::size_t getWidth() const { return m_width; }
static constexpr std::size_t DefaultWidth{ 10 };
static constexpr std::size_t DefaultHeight{ 10 };
private:
void verifyCoordinate(std::size_t x, std::size_t y) const;
std::vector<std::optional<std::string>> m_cells;
std::size_t m_width { 0 }, m_height { 0 };
};
注意不用指向任何变量类型,比如T,在特殊化中:直接在const char*与string上工作。此时一个明显的问题是为什么这个类仍然有一个模板头。也就是说,下面的语法有什么好处?
template <>
class Grid<const char*>
该语法告诉编译器这个类是一个const char*特殊化的Grid类模板。假定你不用这个语法,尝试这样写:
class Grid
编译器不会让你这么做的,因为已经有一个类模板叫Grid了(原来的类模板)。只有特殊化它才能重用这个名字。特殊化的主要益处是对用户是不可见的。当用户生成一个int或SpreadsheetCell的Grid时,编译器从原来的Grid模板生成代码。当用户生成const char*的Grid时,编译器使用const char*特殊化。这些都是在幕后完成的。
主模块接口文件只是导入与导出两个模块接口分区:
export module grid;
export import :main;
export import :string;
特殊化可以测试如下:
Grid<int> myIntGrid; // Uses original Grid template.
Grid<const char*> stringGrid1{ 2, 2 }; // Uses const char* specialization.
const char* dummy{ "dummy" };
stringGrid1.at(0, 0) = "hello";
stringGrid1.at(0, 1) = dummy;
stringGrid1.at(1, 0) = dummy;
stringGrid1.at(1, 1) = "there";
Grid<const char*> stringGrid2{ stringGrid1 };
当特殊化一个类模板时,没有“继承”任何代码;特殊化不像派生。必须重写类的整个实现。不要求提供同样名字或行为的成员函数。作为一个例子,const char*特殊化的Grid实现at()成员函数返回optional<string>,而不是optional<const char*>。实际上,可以写一个与原来无关的完全不同的类。当然了,那是对模板特殊化能力的滥用,没有正当理由不要这么干。下面是const char*特殊化的成员函数的实现。与类模板定义不同,不用重复模板头,template<>在每个成员函数定义的前面。
Grid<const char*>::Grid(std::size_t width, std::size_t height)
: m_width{ width }
, m_height{ height }
{
m_cells.resize(m_width * m_height);
}
void Grid<const char*>::verifyCoordinate(std::size_t x, std::size_t y) const
{
if (x >= m_width) {
throw std::out_of_range { std::format("x ({}) must be less than width ({}).", x, m_width) };
}
if (y >= m_height) {
throw std::out_of_range { std::format("y ({}) must be less than height ({}).", y, m_height) };
}
}
const std::optional<std::string>& Grid<const char*>::at(std::size_t x, std::size_t y) const
{
verifyCoordinate(x, y);
return m_cells[x + y * m_width];
}
std::optional<std::string>& Grid<const char*>::at(std::size_t x, std::size_t y)
{
return const_cast<std::optional<std::string>&>(std::as_const(*this).at(x, y));
}
本节讨论如何使用类模板特殊化来书写类模板的特殊实现,用特定类型替换模板类型参数。这叫做完整模板特殊化。这样的完整类模板特殊化就不再是类模板自身了,而是一个类定义。后面的章节会继续讨论类模板特殊化,有更高级的特性叫做部分特殊化。