Skip to content

对象模型、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 子对象;由于二者各自继承 AD 中有两个独立的 A 子对象

text
D(概念布局)
├─ B 子对象
│  ├─ A 子对象:a、与 B 视图关联的虚调用信息
│  └─ b
├─ C 子对象
│  ├─ A 子对象:a、与 C 视图关联的虚调用信息
│  └─ c
└─ d

因此 D d; d.a 有歧义,必须写 d.B::ad.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:

  1. 使用 final 标记不可再派生的类或不可再重写的函数,帮助编译器在可证明动态类型时去虚拟化并内联;
  2. 热路径中可使用模板/CRTP 等静态多态;
  3. 改善对象布局和批处理方式,减少指针追逐;
  4. 不要为了“手写虚表”牺牲类型安全和可维护性,除非有明确的 ABI/性能需求。

继承多态与 std::variant

维度继承 + 虚函数std::variant
类型集合可在运行时扩展,适合插件式开放层次编译期固定,新增类型需修改 variant 定义
所有权/布局常通过指针/引用使用,对象可能分散值语义,对象通常内联存储,局部性更好
新增操作在基类加虚函数会影响所有派生类std::visit 增加 visitor 即可
新增类型添加新派生类通常较自然需要修改 variant 与所有相关 visitor
运行期开销虚调用、可能有堆分配visit 的分派开销;对象大小取最大备选类型影响

开放的类型维度优先继承;固定的类型集合、数据导向和高局部性场景常适合 variant。两者是不同扩展方向的权衡,不是绝对性能替代关系。

使用 Markdown 与 VitePress 构建