我们已经看到了类型擦除在程序中是如何体现的:代码期望某种语义行为,但与其处理提供该行为的具体类型,我们使用一种抽象,将那些与当前任务无关的类型属性“擦除”(从类型的名称开始)。通过这种方式,类型擦除具有其他几种设计模式的特征,但它并不等同于其中一个。可以合理地认为,类型擦除本身就是一种设计模式。那么,作为设计模式,类型擦除提供了什么?
在类型擦除中,我们找到了对某种特定行为(例如函数调用)的抽象表达,可以用来分离接口与实现。到目前为止,这听起来与继承非常相似。回想一下,在上一节末尾,我们让一个 std::function 对象调用了几种完全不同的可调用对象:一个函数、一个 Lambda 表达式和一个成员函数。这说明了类型擦除与继承之间的根本区别:在继承中,基类决定了抽象行为(接口),需要实现该接口的类都必须从同一个基类派生。而在类型擦除中,没有这样的要求:提供共同行为的类型不必形成特定的层次结构,甚至不必是类。
可以说,类型擦除提供了一种非侵入式的方式来分离接口与实现。所谓“侵入式”,是指我们必须修改一个类型才能使用该抽象:例如,可能有一个类具有所需的行为,但为了能够多态地使用它,还必须从一个公共基类继承。这就是“侵入” —— 必须对一个原本完全正常的类进行强制修改,以便将其用作某个抽象接口的具体实现。正如刚才所看到的,类型擦除没有这样的需求。只要一个类(或其他类型)具有所需的行为 —— 通常是以类似函数调用的方式,用特定参数调用 —— 就可以用来实现这种行为。该类型的其他属性对于支持我们关注的接口并不重要,因此可“擦除”。
我们也可以说,类型擦除提供了“外部多态性”:不需要统一的层次结构,可用于实现特定抽象的类型集合是可扩展的,不仅限于从公共基类派生的类。
那么,为什么类型擦除没有完全取代 C++ 中的继承呢?在某种程度上,这是传统的原因;不过,不要急于抛弃传统 —— 传统的另一个名字是“惯例”,而符合惯例的代码也是熟悉、易于理解的代码。但还有两个“真正”的原因。第一个是性能。我们将在本章后面研究类型擦除的实现及其各自的性能,并且高性能的类型擦除实现最近才出现。第二个是便利性,如果我们需要为一组相关的操作声明一个抽象,可以声明一个具有必要虚成员函数的基类。如果使用 std::function 方法,类型擦除的实现将不得不分别处理这些操作中的每一个。这并不是一个硬性要求 —— 可以一次性实现一个针对整组操作的类型擦除抽象。然而,使用继承更容易实现。此外,所有隐藏在类型擦除背后的具体类型都必须提供所需的行为;如果要求所有这些类型支持几个不同的成员函数,那么它们更有可能因为其他原因而来自同一个层次结构的类型。
并非每次使用类型擦除背后都有宏大的设计思想。很多时候,类型擦除纯粹用作一种实现技术(继承也是如此,我们即将看到这样一个用例)。特别是,类型擦除是打破大型系统中各组件之间依赖关系的绝佳工具。
这里有一个简单的例子。我们正在构建一个大型分布式软件系统,核心组件之一是网络通信层:
class Network {
...
void send(const char* data);
void receive(const char* buffer);
...
}
当然,这是对一个组件的非常简化和抽象的视图,该组件即使在最好的情况下也绝非简单,但我们现在不想关注跨网络发送数据的细节。重要的是,这是一个基础组件,系统的其余部分都依赖于它。我们的软件解决方案可能包含多个不同的程序,都包含这个通信库。
现在,在某个特定应用中,需要在网络传输前后处理数据包;这可能是一个需要高级加密的高安全性系统,或者可能是系统中唯一设计用于在不可靠网络上运行,并需要插入纠错码的工具。关键是,网络层的设计者现在被要求引入对某个外部代码的依赖,而该代码来自一个更高级别的、特定于应用的组件:
class Network {
...
bool needs_processing;
void send(const char* data) {
if (needs_processing) apply_processing(buffer);
...
}
...
};
这段代码看起来很简单,但却带来了依赖关系的噩梦:现在这个底层库必须与特定应用程序中的 apply_processing() 函数一起构建。更糟糕的是,所有其他不需要此功能的程序也必须编译和链接这段代码,即使它们从未设置 needs_processing。
虽然这个问题可以通过“传统”方式解决 —— 使用一些函数指针或(更糟的)全局变量,但类型擦除提供了一个优雅的解决方案:
// Example 05
class Network {
static const char* default_processor(const char* data) {
std::cout << "Default processing" << std::endl;
return data;
}
std::function<const char*(const char*)> processor =
default_processor;
void send(const char* data) {
data = processor(data);
...
}
public:
template <typename F>
void set_processor(F&& f) { processor = f; }
};
这是一个策略设计模式(Strategy Design Pattern)的示例,其中特定行为的实现可以在运行时选择。现在,系统的高层组件都可以指定其自己的处理函数(或 Lambda 表达式,或可调用对象),而无需强制其余软件链接代码:
Network N;
N.set_processor([](const char* s){ char* c; ...; return c; };
现在我们知道了类型擦除的样子,以及如何作为一种设计模式和一种便捷的实现技术来帮助解耦组件,只剩下一个问题了 —— 它是如何工作的?