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 不一致。
↡分配器、对象 destructor 所在代码和最终释放者共同存活的范围;跨域释放必须由显式 owner/deleter 连接。若整个 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 定义。
↡在稳定 element schema 上通过 Accept/Visit 两次分派选择一元 operation,并将遍历策略与分派分开。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 更合适。
↡按两个对象的动态类型 pair 查找 interaction handler,并显式定义 symmetry、unknown pair 与 cast safety。symmetry 是 operation property:overlap 可对称,damage source/target 不对称。key 必须从本次 objects 生成,typed adapter 默认 checked cast;只有 invariant 封闭且 profile 证明 cast material 才考虑 static_cast。
五类契约账本
每个 design review 至少回答:
- Type/key:schema、runtime ID 与 dynamic object 是否一致,版本是否稳定?
- Object/resource:谁拥有、copy/move/clone/cycle 如何,哪个 deleter 最终执行?
- Callable/code:target 与 receiver 是否 live,plugin code 何时可 unload?
- Global lifecycle:谁 stop/drain,destructor 是否还调用别的 service?
- 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 参数化,不是无条件采用最复杂模板。
第一步:按 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:修复热重载事故。 plugin entry 已 unregister,但 callback 和 Node objects 仍 live;给出安全 unload 顺序。
- 问题 2:为新 ScriptNode 选择 Visitor 演化。 core 不能重编译,旧 visitors 可忽略;说明 classic typelist Visitor 为何失败以及替代。
- 问题 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、证明、失败与测试的审计账本。