6.6. 类型擦除使用准则

类型擦除解决了哪些问题?其解决方案的成本在何时可以接受?

首先,最重要的是不要忽视最初的目标:类型擦除是一种设计模式,有助于关注点分离,这是一种非常强大的设计技术。当某种行为的实现可以由一组可能互不相关的开放类型提供时,类型擦除可用于为此行为创建抽象。

它也用作一种实现技术,主要用于帮助打破编译单元和其他程序组件之间的依赖关系。

在我们回答“类型擦除是否值得付出其成本?”这个问题之前,需要考虑其替代方案。

替代方案是实现相同抽象的其他方式:多态类型层次结构或函数指针。这两种选项的性能与类型擦除(在其最佳实现下)相似,因此选择最终取决于便利性和代码质量。对于单个函数,使用类型擦除的函数,比开发一个新的类型层次结构更容易,也比使用函数指针更灵活。而对于具有多个成员函数的类,通常维护一个类型层次结构会更简单且不易出错。

另一种可能的替代方案是不采取措施,允许设计的各个部分之间保持更紧密的耦合。这种决策的缺点往往与其性能优势成反比:系统中紧密耦合的部分,通常需要协调实现以达到良好的性能,但它们之所以紧密耦合是有原因的。逻辑上分离良好的组件不应进行大量交互,这种交互的性能不应是关键因素。

当性能至关重要,但仍然需要抽象时,该怎么办?通常,我们会采取与类型擦除完全相反的方法:将所有内容都变成模板。

以 C++20 的 范围库 为例。一方面,它们是抽象的序列。我们可以编写一个操作 范围 的函数,并用 vector、deque、由这些容器创建的 范围、该 范围 的子 范围 或一个过滤视图来调用它。只要一个对象可以从 begin() 迭代到 end(),它就是 范围。然而,尽管从接口角度看,由 vector 和 deque 创建的 范围 都是序列的抽象,但它们却是不同的类型。标准库提供了多种 范围 适配器和 范围 视图,而它们都是模板,操作这些 范围 的函数也都是模板。

我们能否实现一个类型擦除的范围?可以,而且很简单。最终会得到一个单一类型 GenericRange,可以从 vector、deque、list 或具有 begin()、end() 和前向迭代器的东西构造出来。但也会得到一个性能,大约比大多数容器迭代器慢两倍的东西,vector 除外:vector 的迭代器本质上就是指针,向量化编译器可以进行优化,使代码速度至少提升一个数量级。而当我们擦除了原始容器的类型时,这种优化的可能性就丧失了。

C++ 设计者做出的决策是:一方面,范围 提供了对特定行为的抽象,让我们能够将接口与实现分离;另一方面,不愿意牺牲性能。因此,选择将 范围, 以及所有操作它们的代码都做成模板。

作为软件系统的设计者,可能需要做出类似的决策。通用的指导原则是:对于紧密相关的组件,如果这种耦合对性能至关重要,则优先选择更紧密的耦合;相反,对于松散耦合的组件,如果交互不需要高效率,则优先选择更好的分离。在后一种领域中,类型擦除应至少与多态性和其他解耦技术同等考虑。