Modern C++ Design 总复习:插件式场景运行时

用一个插件式场景运行时贯穿官方11章:Policy/traits/Typelist、allocator、Functor、Singleton、SmartPtr、Factories、Visitor 与 Multimethods,形成可验证的现代化设计闭环。

学习目标

  • 能分析插件式场景系统到官方11章的逐层映射,并说明每章的输入、输出与 runtime state
  • 能设计 type/key、resource owner、callable code、global shutdown 与 memory domain 五类跨章契约审计
  • 能比较 Loki/C++98 与现代 C++ 的替换方案,并以语义、生命周期、成本和演进证据决定发布或拒绝

从一个系统事故开始

场景编辑器支持 runtime plugins。启动时选择 Vulkan/OpenGL backend;plugins 注册 Node creators 与 collision handlers;scene nodes 经 Visitor 执行 serialize/render/validate;两个 objects 碰撞由 Multimethod 决定。一次热重载后,程序在 callback 中跳进已卸载 plugin,退出时 Logger 又访问已销毁 Registry。

这不是“忘了加 shared_ptr”或“Singleton 不好”的单点问题,而是一条跨 11 章的 contract chain:type key 指向哪段 code,谁拥有 object,allocator 在哪个 domain,callback 活多久,global services 何时停止,dispatch 是否仍能找到合法 handler。

Ch1-3:先生成可验证的类型结构

第1章把 backend storage、error checking、thread mode 拆成 Policies;第2章用 concepts/traits 证明 compatibility;第3章用 ProductList/NodeList 生成 factory/visitor schema。目标不是模板数量,而是一个变化轴只有一个 owner。

template<class Storage, class Checking, class Threading>
requires CompatibleScenePolicies<Storage, Checking, Threading>
class SceneRuntime;
 
using BackendProducts = TypeList<Buffer, Texture, Pipeline>;
using NodeKinds = TypeList<MeshNode, LightNode, ScriptNode>;

schema 必须 unique/order/versioned;ConcreteProducts[i] 除了派生于 AbstractProducts[i],还要共享 backend family tag。compile success 不能证明 VulkanBuffer 与 OpenGLPipeline 可混用。

Ch4:Node 内存必须服从 phase lifetime

旧设计为每个 node 使用全局 small-object pool。plugins 卸载时,某些 nodes 仍在 pool;delete 又穿过 plugin-specific destructor。真正问题不是 block 快慢,而是 object lifetime 与 code/memory domain 不一致。

若整个 scene load phase 一起销毁,pmr::monotonic_buffer_resource 更简单;若 nodes 独立回收,pool 要记录 owner module lease。任何 custom allocator 都要验证 size/alignment/constructor throw/thread/cross-free/RSS retention。

Ch5:Functor target 也有代码生命周期

Factory creator、event callback、collision handler 都可放进 type-erased Functor。擦除 concrete type 不会擦除 target code 所在 plugin,也不会自动延长 receiver。

struct RegisteredCallback {
    ModuleLease module;
    std::move_only_function<void(Event&)> invoke;
};
 
void dispatch(RegisteredCallback& callback, Event& event) {
    callback.invoke(event); // lease 保证 target code 仍已加载
}

copyable Functor 若捕获 unique resource 会强迫错误 clone;这里应使用 move-only wrapper。binding/chaining 还要定义 captured state、exception 与 partial effects,不能把 callback 当无状态 function pointer。

Ch6:全局 Registry 要有显式 shutdown

SingletonHolder 可参数化 Creation/Lifetime/Threading,却仍会隐藏依赖。场景系统真正 process-global 的服务只有 ModuleManager/Diagnostics;Registry 与 Renderer 更适合由 application root 持有并按 DAG 停止。

int runApplication() {
    ModuleManager modules;
    Registry registry(modules);
    Renderer renderer(registry);
 
    runEditor(renderer);
    renderer.stop();
    registry.drainAndUnregister();
    modules.unloadAll();
}

这样 dead reference 在 architecture 中消失,而不是用 Phoenix 重生一个空 Registry。Phoenix/longevity 是可用 Policies,但只有 late access 有业务意义且状态可重建时才选。

Ch7:资源所有权与 plugin lease 同步

GPU resources 由 backend family 创建,必须由同 backend/device/deleter 销毁。unique_ptr<Resource, BackendDeleter> 表达独占;shared caches 用 shared_ptr + weak cache,但 control-block thread safety 不等于 GPU object 可并发访问。

不要用 implicit raw conversion 把 owner 静默泄出,也不要重载 operator& 让 C API 覆盖 live handle;使用显式 borrow/out_ptr adapter,review 时能看见 lifetime transition。

Ch8-9:先选 creator,再保证 product family

Node plugin registry 属于 Object Factory:nodeTypeId -> creator. Renderer backend 属于 Abstract Factory:一次选择 Vulkan family,再创建 Buffer/Texture/Pipeline。把两者混成 (backend, product, node) 字符串表会让 caller 重新承担 family consistency。

registry unregister 只阻止新 lookup;必须等待已复制 creators、in-flight calls 和 live products 的 ModuleLeases 全归零。Abstract Factory 若动态切换 theme/backend,应发布完整 immutable family snapshot,而非逐 slot 更新 prototypes。

Ch10:稳定 Node 集合上的一元操作

Mesh/Light/Script nodes 在一个 engine version 内稳定,Serialize/Render/Validate operations 经常增加,适合 generated cyclic Visitor。Accept 只 dispatch 当前 element,traversal order 由独立 walker 定义。

plugin 若新增未知 Node 而 core 不重编译,则 classic root ABI 不再合适;可用 acyclic Visitor/default registry。选择取决于 element dimension 是否 closed,不是“Visitor 更面向对象”。

Ch11:两个动态对象的交互

碰撞处理依赖 lhs/rhs 两个 dynamic types,属于 Multimethod。几种 node types 且 pair dense/hot,可分配 type IDs 使用 matrix;plugin types 稀疏且开放,则 type_index pair registry 更合适。

symmetry 是 operation property:overlap 可对称,damage source/target 不对称。key 必须从本次 objects 生成,typed adapter 默认 checked cast;只有 invariant 封闭且 profile 证明 cast material 才考虑 static_cast。

五类契约账本

每个 design review 至少回答:

  1. Type/key:schema、runtime ID 与 dynamic object 是否一致,版本是否稳定?
  2. Object/resource:谁拥有、copy/move/clone/cycle 如何,哪个 deleter 最终执行?
  3. Callable/code:target 与 receiver 是否 live,plugin code 何时可 unload?
  4. Global lifecycle:谁 stop/drain,destructor 是否还调用别的 service?
  5. Memory domain:size/alignment/allocator owner 与 object lifetime 是否一致?

现代化发布门

优先标准设施:concepts/type_traits、parameter packs/variant、pmr、function/move_only_function、unique/shared/weak_ptr、magic static/call_once、type_index。自定义 Policy host/dispatcher 只有在标准语义不够、ABI/性能/领域约束明确且有测试时保留。

先预测:自研 dense multimethod 让 microbenchmark 快 20%,但增加 plugin type-ID reuse bug、matrix rebuild lock 和 unload complexity;若 collision 只占 frame 1%,端到端上限不足,应拒绝。Modern C++ Design 教会的是让 trade-off 参数化,不是无条件采用最复杂模板。

分步1 / 3

第一步:按 11 章画出系统链

从 compile-time schema 到 runtime substrate、creation、dispatch 逐层标组件;每个 chapter 必须对应实际决定或明确“不使用”。

小结

  • Ch1-3 建立 Policy/traits/type schema,Ch4-7 建立 memory/call/global lifetime/resource owner,Ch8-11 建立 creation 与 dispatch
  • type erasure 隐藏 callable type,不会隐藏 target code lifetime;registry unregister 也不会自动证明 plugin 可卸载
  • Singleton lifecycle 应优先变成 composition-root shutdown,Phoenix/longevity 只处理明确 late-access 语义
  • Object Factory 选择一个 creator,Abstract Factory 保证一族 products;Visitor 处理稳定 elements 上的一元 operations,Multimethod 处理多个动态对象交互
  • 五类 contract ledger 把模板、runtime key、ownership、code lease、shutdown 与 allocator domain 联成可验证链
  • 现代化应先使用标准设施;自定义 Loki-style component 必须通过语义、生命周期、成本与演进门

练习

  1. 问题 1:修复热重载事故。 plugin entry 已 unregister,但 callback 和 Node objects 仍 live;给出安全 unload 顺序。
  1. 问题 2:为新 ScriptNode 选择 Visitor 演化。 core 不能重编译,旧 visitors 可忽略;说明 classic typelist Visitor 为何失败以及替代。
  1. 问题 3:拒绝或发布 custom allocator + dispatcher。 microbench 各快 2x,端到端提升 3%,RSS 增 35%,plugin unload tests 偶发失败;做决定。

名词解释

名词解释

本章出现的专业名词,用大白话再讲一遍。

cross-component contract chain
连接类型、对象、代码、内存与全局生命周期的系统依赖链。
generated type schema
由类型列表驱动接口、factory 或 visitor 生成的编译期 schema。
allocation domain
allocator、object destructor code 与最终释放者共同有效的范围。
callable module lease
保证 callback target code 与 receiver 在调用期间可用的模块凭证。
explicit shutdown phase
停止新请求、排空、注销并按依赖释放的显式阶段。
resource-owner bundle
handle、deleter、backend/device 与 module lease 同步移动销毁的 owner。
two-level creation architecture
Object Factory 选择 creator、Abstract Factory 约束 family 的两级结构。
node Visitor boundary
稳定 node schema 上的双分派边界,与 traversal 分离。
interaction dispatcher
按动态类型 pair 选择 interaction handler 的分派器。
contract ledger
记录五类资产 owner、证明、失败与测试的审计账本。

讨论

评论区加载中…