Item41--了解隐式接口和编译期多态
.两种编程范式的对比
Scott Meyers 在本条款开头提出了一个重要的视角转换:
- 面向对象编程 (OOP) 的世界里,我们习惯于 显式接口 (Explicit Interfaces) 和 运行期多态 (Runtime Polymorphism)。
- 泛型编程 (Generic Programming) 的世界里,重心转移到了 隐式接口 (Implicit Interfaces) 和 编译期多态 (Compile-time Polymorphism)。
2. 显式接口与运行期多态 (OOP)
这是我们最熟悉的模式。假设有一个类 Widget:
class Widget {
public:
Widget();
virtual ~Widget();
virtual std::size_t size() const;
virtual void normalize();
void swap(Widget& other);
// ...
};
void doProcessing(Widget& w) {
if (w.size() > 10 && w != someNastyWidget) {
Widget temp(w);
temp.normalize();
temp.swap(w);
}
}
在这个例子中:
- 显式接口 (Explicit Interface): 我们可以在源码中清晰地看到
Widget类的定义(头文件)。如果要调用w.size(),Widget类必须明确声明一个名为size的函数,且参数匹配。 - 运行期多态 (Runtime Polymorphism): 因为
size和normalize是虚函数(virtual),w具体调用哪个版本的函数,是在程序运行时根据w实际指向的动态类型(Dynamic Type)决定的。
3. 隐式接口与编译期多态 (Templates)
现在,我们将 doProcessing 改写为函数模板:
template<typename T>
void doProcessing(T& w) {
if (w.size() > 10 && w != someNastyWidget) {
T temp(w);
temp.normalize();
temp.swap(w);
}
}
在这个模板函数中,T 是什么?我们不知道。但是,通过观察函数体内的代码,我们可以推断出 T 必须满足的一系列“约束条件”。
什么是隐式接口 (Implicit Interface)?
T 的接口不是由类定义显式给出的,而是由模板函数中有效表达式 (Valid Expressions) 组成的集合决定的。
在上面的代码中,T 的隐式接口包括:
- 必须提供
size()成员函数。 size()的返回值必须能与10(int)进行>比较。(注意:返回值不必是 int,只要能和 int 比较即可)。- 必须支持
!=运算符,用来和someNastyWidget比较。 - 必须支持拷贝构造(
T temp(w))。 - 必须支持
normalize()成员函数。 - 必须支持
swap()成员函数。
这些约束加在一起,构成了 T 的隐式接口。如果传入的类型不支持这些操作,代码将无法通过编译。
什么是编译期多态 (Compile-time Polymorphism)?
当你实例化模板 doProcessing<Widget>(w) 或 doProcessing<int>(i) 时,编译器会根据传入的具体类型,生成不同的函数版本。
- 这种根据不同类型改变行为的过程,就是多态。
- 因为它发生在编译阶段(Template Instantiation),所以称为编译期多态。
- 这类似于函数重载(Overloading),但它是模板实例化机制驱动的。
4. 核心区别总结
| 特性 | 面向对象 (Classes) | 泛型编程 (Templates) |
|---|---|---|
| 接口性质 | 显式 (Explicit) 由函数签名(名称、参数、返回类型)定义。 | 隐式 (Implicit) 由有效表达式(valid expressions)定义。 |
| 多态发生时间 | 运行期 (Runtime) 通过虚函数表 (vptr/vtbl) 查找。 | 编译期 (Compile-time) 通过模板实例化和函数重载解析。 |
| 检查时机 | 编译时检查签名是否匹配,但具体调用在运行时决定。 | 编译时检查所有表达式是否合法。如果不合法,直接报错。 |
5. 为什么这很重要?
理解这一点的关键在于理解 “表达式”的灵活性。
在显式接口中,如果函数签名是 int size() const,那么它必须返回 int(或可转换类型)。
但在隐式接口中,约束宽松得多。例如表达式:
if (w.size() > 10)
对于模板类型 T:
T的size()不需要返回int。- 它可以返回一个复杂的对象
X。 - 只要这个对象
X重载了operator>,且该操作符能接受一个int作为右侧参数,这个表达式就是合法的。
这意味着:隐式接口仅仅由“表达式是否有效”来定义,而不拘泥于具体的函数签名。
6. 现代 C++ 的补充 (C++20 Concepts)1
虽然《Effective C++》写于 C++98/03 时代,但这个条款直接引出了现代 C++ 的一个重要特性:Concepts (概念)。
在 C++20 之前,如果模板实例化时类型不符合隐式接口,编译器会报出极其冗长、难以阅读的错误信息(Template Error Explosion)
C++20 引入了 Concepts,允许我们显式地定义隐式接口。例如:
template<typename T>
concept Sizable = requires(T a) {
{ a.size() } -> std::convertible_to<std::size_t>;
};
// 显式约束 T 必须满足 Sizable 概念
template<Sizable T>
void doProcessing(T& w) { ... }
这实际上是将 Item 41 提到的隐式接口显式化了,从而获得更好的编译错误信息和代码可读性。
总结 (Takeaway)
- 类 (Classes) 和模板 (Templates) 都支持接口和多态。
- 类使用显式接口(由函数签名定义)和运行期多态(虚函数)。
- 模板使用隐式接口(由有效表达式定义)和编译期多态(模板实例化)。
- 编写模板代码时,要习惯于思考“这个类型需要支持哪些表达式”,而不是“这个类型继承自什么基类”。
浙公网安备 33010602011771号