Appearance
对象模型、RTTI 与多态设计
C++ 标准规定对象语义,但不规定对象内部必须有 vptr/vtable,也不规定成员、基类子对象在内存中的精确偏移。下面的布局图描述主流 ABI 的常见实现,可用于理解,不应当作跨编译器二进制协议。
多重继承与菱形继承
cpp
class A { public: virtual void f1(); int a = 1; };
class B : public A { public: virtual void f2(); int b = 2; };
class C : public A { public: virtual void f3(); int c = 3; };
class D : public B, public C { public: virtual void f4(); int d = 4; };D 使用普通多重继承时含有一个 B 子对象和一个 C 子对象;由于二者各自继承 A,D 中有两个独立的 A 子对象:
text
D(概念布局)
├─ B 子对象
│ ├─ A 子对象:a、与 B 视图关联的虚调用信息
│ └─ b
├─ C 子对象
│ ├─ A 子对象:a、与 C 视图关联的虚调用信息
│ └─ c
└─ d因此 D d; d.a 有歧义,必须写 d.B::a 或 d.C::a。主流实现可能为不同基类子对象保留不同的 vptr;从 D* 转换到 C* 也可能需要调整指针。但具体虚表条目和对象大小是 ABI 细节。
虚继承
cpp
class B : virtual public A {};
class C : virtual public A {};
class D : public B, public C {};虚继承让最派生对象 D 中只保留一个共享的 A 虚基类子对象,从而消除菱形继承中基类状态重复和访问歧义。
- 虚基类由最派生类负责构造;
D必须决定如何初始化A; - 编译器需要额外的偏移或间接访问元数据来定位共享虚基类;
- 对象布局、某些转换和访问可能更复杂,大小/性能成本依 ABI 和实际使用而定,不应脱离测量声称“必然显著变慢”。
接口型多重继承(多个无状态纯虚接口)通常不需要虚继承;只有确实需要共享同一份基类状态时再考虑。
RTTI 与 dynamic_cast
RTTI(运行时类型信息)允许在多态层次中查询对象动态类型。dynamic_cast 常用于安全的下行转换和交叉转换:
cpp
struct Base { virtual ~Base() = default; };
struct Derived : Base { void only_in_derived(); };
Base* base = /* ... */;
if (auto* derived = dynamic_cast<Derived*>(base)) {
derived->only_in_derived();
}要点:
- 对多态指针/引用进行运行时检查时,源类型通常需要至少一个虚函数;
- 指针转换失败返回
nullptr;引用转换失败抛出std::bad_cast; - 在多重继承中,它会根据实际对象调整到正确的基类/派生类子对象地址;
- 编译器通常通过 RTTI 元数据和继承层次信息实现,具体形式不由标准规定。
频繁 dynamic_cast 往往意味着调用方依赖具体类型。若操作本质是“不同对象执行不同动作”,优先设计虚函数;若类型集合固定且希望值语义,可考虑 std::variant。但 dynamic_cast 不是禁用项:插件边界、异构对象集合或少量诊断路径中,它能带来清晰的安全检查。
虚函数的性能与去虚拟化
一次虚调用常涉及读取 vptr、间接取得目标函数并间接跳转。潜在成本包括间接分支预测、难以内联以及对象分散导致的缓存未命中;但在非热点业务路径中,这些开销通常远小于 I/O、分配或实际业务计算。
优化应先 profile:
- 使用
final标记不可再派生的类或不可再重写的函数,帮助编译器在可证明动态类型时去虚拟化并内联; - 热路径中可使用模板/CRTP 等静态多态;
- 改善对象布局和批处理方式,减少指针追逐;
- 不要为了“手写虚表”牺牲类型安全和可维护性,除非有明确的 ABI/性能需求。
继承多态与 std::variant
| 维度 | 继承 + 虚函数 | std::variant |
|---|---|---|
| 类型集合 | 可在运行时扩展,适合插件式开放层次 | 编译期固定,新增类型需修改 variant 定义 |
| 所有权/布局 | 常通过指针/引用使用,对象可能分散 | 值语义,对象通常内联存储,局部性更好 |
| 新增操作 | 在基类加虚函数会影响所有派生类 | 用 std::visit 增加 visitor 即可 |
| 新增类型 | 添加新派生类通常较自然 | 需要修改 variant 与所有相关 visitor |
| 运行期开销 | 虚调用、可能有堆分配 | visit 的分派开销;对象大小取最大备选类型影响 |
开放的类型维度优先继承;固定的类型集合、数据导向和高局部性场景常适合 variant。两者是不同扩展方向的权衡,不是绝对性能替代关系。