第11章:Multimethods
对齐第一版第11章 Multimethods:双动态类型需求、brute-force 与自动化、对称分派、对数 FnDispatcher、Functor、cast 安全、常数时间引擎及 BasicDispatcher Policies。
学习目标
- 能解释 what/when multimethods are needed,并把 operation key 从单 receiver type 扩展为多个动态参数类型 tuple
- 能实现 brute-force、logarithmic FnDispatcher 与 constant-time dispatcher,处理 symmetry、unknown pair、registration 和 callable storage
- 能判断 static_cast/dynamic_cast 的证明条件,并用 BasicDispatcher/BasicFastDispatcher Policies 比较速度、内存、开放扩展与安全
机制总览
第11章:Multimethods:机制路径
- 1
为什么需要先问 What Are Multimethods?
What Are Multimethods? 普通 virtual method 只按一个 receiver 的 dynamic type 选择;multimethod 根据两个或更多 arguments 的 dynamic types 共同选择 operation。CLOS 等语言原生支持,C++…
- 2
When Are Multimethods Needed?
When Are Multimethods Needed? 要同时满足:参与对象来自开放/多态 hierarchy;operation 真正依赖多个 dynamic types;把逻辑放进任一 class 都造成 cross-type coupling;组合数量足以让 hand-written conditionals 漂移。
- 3
Double Switch-on-Type:Brute F…
Double Switch-on-Type: Brute Force 对第一个 object 逐类 dynamic cast,命中后再对第二个逐类 cast。N 种 types 最坏有 N² pairs,central function 知道所有 concrete classes。
章级决策实验
第11章:Multimethods:机制与证据
切换《第11章:Multimethods》的三个关键教学阶段,先解释机制,再用运行与失败证据验证结论。
选择推理阶段
当前阶段 · 为什么需要先问 What Are Multimethods?
What Are Multimethods? 普通 virtual method 只按一个 receiver 的 dynamic type 选择;multimethod 根据两个或更多 arguments 的 dynamic types 共同选择 operation。CLOS 等语言原生支持,C++…
可核验证据
用正向与应拒绝的编译案例、生成类型和生命周期测试核对「为什么需要先问 What Are Multimethods?」的组合规则与扩展边界。
学完《第11章:Multimethods》后,应能从输入和前置条件推导状态变化,并用可重复的构建、运行或边界测试证明结果。
失效—证据矩阵
第11章:Multimethods:失效与核验
为什么需要先问 What Are Multimethods?
典型失效
若只复制「为什么需要先问 What Are Multimethods?」模板结构而不声明替换点、所有权和实例化边界,组合后的类型会迅速产生二义性或不可诊断错误。
核验证据
用正向与应拒绝的编译案例、生成类型和生命周期测试核对「为什么需要先问 What Are Multimethods?」的组合规则与扩展边界。
When Are Multimethods Needed?
典型失效
若只复制「When Are Multimethods Needed?」模板结构而不声明替换点、所有权和实例化边界,组合后的类型会迅速产生二义性或不可诊断错误。
核验证据
用正向与应拒绝的编译案例、生成类型和生命周期测试核对「When Are Multimethods Needed?」的组合规则与扩展边界。
Double Switch-on-Type:Brute F…
典型失效
若只复制「Double Switch-on-Type:Brute F…」模板结构而不声明替换点、所有权和实例化边界,组合后的类型会迅速产生二义性或不可诊断错误。
核验证据
用正向与应拒绝的编译案例、生成类型和生命周期测试核对「Double Switch-on-Type:Brute F…」的组合规则与扩展边界。
为什么需要先问 What Are Multimethods?
What Are Multimethods? 普通 virtual method 只按一个 receiver 的 dynamic type 选择;multimethod 根据两个或更多 arguments 的 dynamic types 共同选择 operation。CLOS 等语言原生支持,C++ 需要 Visitor、RTTI registry 或 generated table 模拟。
↡根据多个参数的运行时类型组合选择最具体操作的多重动态分派机制。collision 是典型例子:collide(GameObject&, GameObject&) 对 Ship/Asteroid、Ship/Station、Asteroid/Asteroid 的行为不同。把方法放进 lhs class 仍只 dispatch lhs,rhs 要再判断;把方法放进 operation object 也面临同样问题。
When Are Multimethods Needed?
When Are Multimethods Needed? 要同时满足:参与对象来自开放/多态 hierarchy;operation 真正依赖多个 dynamic types;把逻辑放进任一 class 都造成 cross-type coupling;组合数量足以让 hand-written conditionals 漂移。
↡当操作语义由多个独立多态对象的具体类型共同决定、且单分派归属会产生耦合时的使用条件。如果一边类型集合很小且稳定,Visitor double dispatch 更简单;若数据本来是 closed variant, std::visit 可在 compile time 穷举组合;若行为主要由值/状态而非 type 决定,规则引擎或 ordinary branching 更合适。
Double Switch-on-Type:Brute Force
Double Switch-on-Type: Brute Force 对第一个 object 逐类 dynamic_cast,命中后再对第二个逐类 cast。N 种 types 最坏有 N² pairs,central function 知道所有 concrete classes。
↡通过嵌套 dynamic type tests 穷举两个参数具体类型组合并直接调用对应函数的方法。void collide(GameObject& lhs, GameObject& rhs) {
if (auto* ship = dynamic_cast<Ship*>(&lhs)) {
if (auto* asteroid = dynamic_cast<Asteroid*>(&rhs)) {
return collideShipAsteroid(*ship, *asteroid);
}
if (auto* station = dynamic_cast<Station*>(&rhs)) {
return dockOrReject(*ship, *station);
}
}
if (auto* asteroid = dynamic_cast<Asteroid*>(&lhs)) {
if (auto* other = dynamic_cast<Asteroid*>(&rhs)) {
return collideAsteroids(*asteroid, *other);
}
}
throw UnknownCollision{};
}优点是可读、无需 registry,compiler 能看见所有 branches;缺点是 extension 修改 central code、pair symmetry 重复、test coverage 与顺序容易漏。对少量 types 它可能正是 pragmatic baseline。
The Brute-Force Approach Automated
The Brute-Force Approach Automated 用 Typelist 与 template recursion 生成嵌套 type tests。算法按 list1 尝试 lhs,再按 list2 尝试 rhs,命中后调用 typed executor;列表末端走 error policy。
↡以类型列表生成嵌套 dynamic type tests,消除手写 pair 分支但保留穷举查找性质的 dispatcher。自动化保证 list 中每种 type 都进入结构,却不保证每个 pair 有合理语义。default executor、compile-time coverage matrix 或 explicit unsupported set 仍需定义。模板展开还会增加 code size 和 compile diagnostics。
Symmetry with the Brute-Force Dispatcher
Symmetry with the Brute-Force Dispatcher 处理 f(A,B) == f(B,A) 的 operation。dispatcher 可把 type pair canonicalize,例如按 type index 排序;若交换 inputs,还要在调用 typed handler 时恢复它期望的 argument roles。
不是所有碰撞都对称:Ship docking Station 与 Station docking Ship 可能角色不同;damage source/target 更明显。symmetry 是 operation property,不是 type pair property。同一个 pair 对 overlap test 可对称,对 apply-damage 可不对称。
The Logarithmic Double Dispatcher
The Logarithmic Double Dispatcher 用 ordered map,将 (type_info(lhs), type_info(rhs)) 映射到 callback。lookup O(log M),M 是已注册 pairs;新增 concrete type/operation 通过 registration,不改 central chain。
using Key = std::pair<std::type_index, std::type_index>;
using Callback = std::function<void(GameObject&, GameObject&)>;
class Dispatcher {
public:
template<class L, class R, class F>
void add(F function) {
table_[{typeid(L), typeid(R)}] =
[fn = std::move(function)](GameObject& l, GameObject& r) {
fn(dynamic_cast<L&>(l), dynamic_cast<R&>(r));
};
}
private:
std::map<Key, Callback> table_;
};registry lookup 命中后 adapter 仍做 checked cast,验证 key/object agreement。若 callback code 来自 plugin,registration entry、in-flight call 与 objects 都要持 module lease;问题与第 8 章 Factory 相同。
FnDispatcher and Symmetry
FnDispatcher and Symmetry 将 pair normalization 封装在 registration/lookup:对 symmetric operation,只注册 canonical key;调用顺序反转时,adapter 交换 objects 后再传 typed function。unknown pair 由 error function、default callback 或 no-op 处理。
↡保存 function callbacks、处理 type-pair lookup 与可选 symmetry normalization 的运行时 dispatcher。canonical ordering 不能依赖 type_info::name();同进程可用 std::type_index strict order 或内部 type IDs。若 IDs 来自 registration 顺序,多线程/plugin load order 会影响数值但不应影响语义。
Double Dispatch to Functors
Double Dispatch to Functors 用第 5 章 generalized Functor 取代裸函数指针:handler 可捕获 state、调用 member function、组合 logging/metrics。统一 erased signature 常为 Base&,Base&; registration adapter 保留 typed L&,R& target。
Functor 带来 heap/indirect call/copy semantics;dispatcher table 本身也有 allocation。operation 很重时可忽略,physics narrow phase 每秒百万次则应测 direct matrix、function pointer 与 inlined closed variant。
Converting Arguments:static_cast or dynamic_cast?
Converting Arguments: static_cast or dynamic_cast? 取决于 registry invariant 是否足以证明 object dynamic types 与 key 相同。dynamic_cast<L&> 检查并在 mismatch 抛 bad_cast; static_cast<L&> 在证明错误时产生 undefined behavior。
template<class L, class R, class F>
auto checkedAdapter(F fn) {
return [fn = std::move(fn)](Base& left, Base& right) {
auto* l = dynamic_cast<L*>(&left);
auto* r = dynamic_cast<R*>(&right);
if (!l || !r) throw DispatcherInvariantViolation{};
return fn(*l, *r);
};
}可先在 debug/validation build 用 dynamic_cast,在 production 若 measurement 证明 cast 显著且 key 由 typeid(objects) 当场生成,可在封闭 adapter 内 static_cast;不要让 unsafe cast 扩散到 handlers。多重/虚继承还需要 pointer adjustment,static_cast from correct Base can perform it,但前提依旧是 dynamic type 确定。
Constant-Time Multimethods:Raw Speed
Constant-Time Multimethods: Raw Speed 为每个 concrete type 分配 dense integer ID,用二维 table handlers[idL][idR] 直接索引;或 unordered map pair 期望 O(1)。dense matrix 速度稳定,但 memory O(N²),sparse operations 浪费大量 slots。
type ID 分配、table resize、plugin unload 和 concurrent registration 成为新成本。IDs 若复用,live objects 的旧 ID 会误命中新 type;需要 generation/version 或 process-lifetime non-reused IDs。matrix publish 可用 immutable snapshot 避免 lookup lock。
BasicDispatcher and BasicFastDispatcher as Policies
BasicDispatcher and BasicFastDispatcher as Policies 把 lookup engine 作为 Policy:前者可用 map/logarithmic storage,后者用 hash/dense table;外层 Multimethod 保持 register/go/error/symmetry API。其他 Policies 可定义 cast、callback、unknown 与 threading。
↡分别封装通用对数查找与快速常数查找的数据结构 Policy,供同一 multimethod Host 选择。Policy compatibility 要求 key/callback/registration semantics 一致。Fast policy 若不支持 unregister 或 stable iterator,Host 不能假定这些能力;可用 concepts 表达 optional operations。切换 storage policy 不应改变 symmetric/default handler 等业务语义。
Looking Forward
Looking Forward 到现代 C++,closed type sets 可用 std::variant + std::visit 生成 compile-time Cartesian dispatch;open sets 可用 type_index registry;性能敏感 ECS 常用 explicit type IDs/tables;future language pattern matching 进一步改善 closed sum types。
先预测:8 types 的 dense matrix 只有 64 slots,constant-time 简单;500 plugin types 有 250,000 pairs,实际只注册 800 个,map/hash 更省内存。先量 N、M、dispatch frequency、operation cost 和 mutation frequency,再选 engine。
第一步:确认多分派与 symmetry
列出 dynamic arguments、所有已知 pairs、unsupported behavior,并对每个 operation 单独判断交换参数是否等价。
小结
- multimethod 根据多个动态参数类型 tuple 选择操作,适用于 cross-type behavior 无自然单一 owner 的场景
- brute-force double switch 是清楚 baseline,Typelist 可自动生成但仍有 O(N²) 测试/代码结构
- symmetry 属于 operation;canonical pair 必须在反转输入时保持 typed argument roles
- logarithmic dispatcher 用 type pair map,FnDispatcher/Functor 支持开放注册与 stateful callbacks
- argument conversion 应默认 checked,只有 key/object invariant 封闭可证明且测得收益时才考虑 static_cast
- constant-time matrix 用速度换 O(N²) memory 与 type-ID lifecycle;sparse open sets 更适合 map/hash
- BasicDispatcher/BasicFastDispatcher 将 lookup storage Policy 化,外层语义应保持一致
- closed sets 可用 variant visit,open plugin sets 可用 registry,ECS/热路径可用 dense IDs;先按 N、M、频率与扩展选择
练习
- 问题 1:判断 symmetry。 碰撞 overlap query、damage application、chemical reaction 三种 operation 是否可把 (A,B)/(B,A) 合并?
- 问题 2:选择 lookup engine。 12 个 closed types,144 pairs 中 120 有处理,每 frame 调 200 万次;另一个 plugin 系统 400 types 只注册 600 pairs且启动后很少调用,分别选择。
- 问题 3:证明 cast 安全。 dispatcher key 当场由
typeid(lhs/rhs)生成,entry 只由 typed registration adapter 创建;说明 static_cast 仍需哪些边界测试。
名词解释
名词解释
本章出现的专业名词,用大白话再讲一遍。
- multimethod
- 按多个运行时参数类型组合选择操作的动态分派。
- multimethod need
- cross-type behavior 同时依赖多个多态类型且单一归属造成耦合的条件。
- brute-force double dispatch
- 用嵌套类型测试穷举两个动态参数组合的方法。
- automated brute-force dispatcher
- 由类型列表生成嵌套 tests 的自动化穷举引擎。
- symmetric dispatch
- 将交换参数语义相同的两个有序 pair 合并为一个处理项。
- logarithmic double dispatcher
- 在有序 type-pair map 中对数查找 callback 的引擎。
- FnDispatcher
- 保存 function callbacks 并处理 pair lookup/symmetry 的分派器。
- Functor multimethod
- 把 type pair 映射到可保存状态的擦除 callable。
- dispatch argument conversion
- 从 Base references 恢复 registered concrete references 的转换边界。
- constant-time multimethod
- 用 dense type IDs 与 table 常数时间选择 handler 的引擎。
- dispatcher storage Policy
- 封装 map/hash/matrix 查找结构供同一 Host 选择的策略。
- multimethod design horizon
- 按开放性、扩展和性能选择现代多分派表示的视角。