第10章:Visitor
对齐第一版第10章 Visitor:双分派基础、catch-all overload、Acyclic Visitor、Typelist 生成的通用实现、Cyclic Visitor 回归与返回值/默认处理等 variations。
学习目标
- 能解释 Visitor basics 中 single dispatch 的缺口,并逐步推导 Element::Accept 与 Visitor::Visit 的 double dispatch
- 能实现 catch-all overload、Acyclic Visitor 与 Typelist-generated generic Visitor,说明 unsupported type 与返回值协议
- 能比较 cyclic/acyclic/generated variations 在新增 element、operation、RTTI、性能和版本兼容上的成本
机制总览
第10章:Visitor:机制路径
- 1
为什么一次 virtual call 不够
Visitor Basics 解决“操作取决于两个动态类型”:一边是 Shape 实际指向 Circle/Polygon,另一边是 Serialize/Render/Validate operation。C++ virtual dispatch 默认只按 receiver 的动态类型选择;function overload 按参数静态类型选择。
- 2
Visitor 的适用扩展方向
Visitor 把 operation 从 element classes 中抽出,新增 operation 只需新 Visitor;但新增 element 要给 Visitor base 增 virtual method,并修改所有 concrete visitors。它适合 element hi…
- 3
Overloading:The Catch-All Fun…
Overloading: The Catch-All Function 为未列出的 element 提供 fallback,例如 visit(Base&) 或 template catch-all。由于 overload resolution 发生在 concrete accept 中,exact …
章级决策实验
第10章:Visitor:机制与证据
切换《第10章:Visitor》的三个关键教学阶段,先解释机制,再用运行与失败证据验证结论。
选择推理阶段
当前阶段 · 为什么一次 virtual call 不够
Visitor Basics 解决“操作取决于两个动态类型”:一边是 Shape 实际指向 Circle/Polygon,另一边是 Serialize/Render/Validate operation。C++ virtual dispatch 默认只按 receiver 的动态类型选择;function overload 按参数静态类型选择。
可核验证据
用正向与应拒绝的编译案例、生成类型和生命周期测试核对「为什么一次 virtual call 不够」的组合规则与扩展边界。
学完《第10章:Visitor》后,应能从输入和前置条件推导状态变化,并用可重复的构建、运行或边界测试证明结果。
失效—证据矩阵
第10章:Visitor:失效与核验
为什么一次 virtual call 不够
典型失效
若只复制「为什么一次 virtual call 不够」模板结构而不声明替换点、所有权和实例化边界,组合后的类型会迅速产生二义性或不可诊断错误。
核验证据
用正向与应拒绝的编译案例、生成类型和生命周期测试核对「为什么一次 virtual call 不够」的组合规则与扩展边界。
Visitor 的适用扩展方向
典型失效
若只复制「Visitor 的适用扩展方向」模板结构而不声明替换点、所有权和实例化边界,组合后的类型会迅速产生二义性或不可诊断错误。
核验证据
用正向与应拒绝的编译案例、生成类型和生命周期测试核对「Visitor 的适用扩展方向」的组合规则与扩展边界。
Overloading:The Catch-All Fun…
典型失效
若只复制「Overloading:The Catch-All Fun…」模板结构而不声明替换点、所有权和实例化边界,组合后的类型会迅速产生二义性或不可诊断错误。
核验证据
用正向与应拒绝的编译案例、生成类型和生命周期测试核对「Overloading:The Catch-All Fun…」的组合规则与扩展边界。
为什么一次 virtual call 不够
Visitor Basics 解决“操作取决于两个动态类型”:一边是 Shape* 实际指向 Circle/Polygon,另一边是 Serialize/Render/Validate operation。C++ virtual dispatch 默认只按 receiver 的动态类型选择;function overload 按参数静态类型选择。
struct Circle;
struct Polygon;
struct Visitor {
virtual void visit(Circle&) = 0;
virtual void visit(Polygon&) = 0;
virtual ~Visitor() = default;
};
struct Shape {
virtual void accept(Visitor&) = 0;
virtual ~Shape() = default;
};
struct Circle : Shape {
void accept(Visitor& v) override { v.visit(*this); }
};第一次 dispatch 由 shape->accept 找到 Circle::accept,此时 *this 的静态类型已是 Circle&;第二次 v.visit(Circle&) 才选择 operation 的 Circle overload。所谓 double dispatch 是两次 single dispatch 的协议组合。
Visitor 的适用扩展方向
Visitor 把 operation 从 element classes 中抽出,新增 operation 只需新 Visitor;但新增 element 要给 Visitor base 增 virtual method,并修改所有 concrete visitors。它适合 element hierarchy 稳定、operations 经常增加的编译器 AST、document tree、scene graph。
↡将一族异构 element 上的操作集中到独立对象,使 operation dimension 易扩展而 element dimension相对封闭的模式。若 elements 频繁由 plugins 增加,classic Visitor 会形成 versioning bottleneck;variant + std::visit、type-erased callbacks、multimethod registry 或 virtual methods 可能更合适。Pattern 选择必须匹配变化轴。
Overloading:The Catch-All Function
Overloading: The Catch-All Function 为未列出的 element 提供 fallback,例如 visit(Base&) 或 template catch-all。由于 overload resolution 发生在 concrete accept 中,exact visit(Circle&) 比 base fallback 更优。
struct LoggingVisitor : Visitor {
using Visitor::visit;
void visit(Shape& unknown) { log(unknown.typeName()); }
void visit(Circle& circle) override { log(circle.radius()); }
};现实中 base Visitor 若只声明 pure exact overload,derived 新增 catch-all 不会自动满足它们;需要 interface 设计支持 fallback,或每个 exact overload default-forward 到 visit(Shape&)。using 也用于防止 name hiding,让 base overload set 可见。
An Implementation Refinement:The Acyclic Visitor
Classic Visitor interface 引用所有 concrete elements,形成 dependency cycle:Visitor knows Circle/Polygon,Circle/Polygon 又知道 Visitor。新增 element 改动 root interface 与所有 operations。The Acyclic Visitor 将每种 element 的访问协议拆成独立 Visitor<T> interface。
struct BaseVisitor { virtual ~BaseVisitor() = default; };
template<class T>
struct VisitorFor {
virtual void visit(T&) = 0;
};
struct Circle : Shape {
void accept(BaseVisitor& base) override {
if (auto* visitor = dynamic_cast<VisitorFor<Circle>*>(&base)) {
visitor->visit(*this);
} else {
onUnsupported(base);
}
}
};
struct Render : BaseVisitor, VisitorFor<Circle>, VisitorFor<Polygon> {
void visit(Circle&) override;
void visit(Polygon&) override;
};new element 只定义新的 VisitorFor<New>,已有 operations 不必重编译/实现;operation 也可只支持 subset。代价是每次 accept dynamic_cast,unsupported path 变成 runtime policy,interface inheritance 与 RTTI 增加复杂度。
A Generic Implementation of Visitor
A Generic Implementation of Visitor 用 Typelist 生成 visitor interfaces,避免手写每个 overload。GenScatterHierarchy<TList, VisitorUnit> 生成并列 Visit(T&) units;acyclic 版本可让 operation 从所需 units 继承,cyclic 版本由完整 list 生成 root。
template<class T, class R>
struct VisitorUnit {
virtual R visit(T&) = 0;
};
template<class R, class ElementList>
using GeneratedVisitor =
GenScatterHierarchy<ElementList, BindSecond<VisitorUnit, R>::template Type>;真实模板需要把 R 绑定进 Unit,并为 virtual destructor、const element、reference/pointer 和 default behavior 定义协议。Product list duplicate 会生成 ambiguous bases;return type 不同的 operations 也不能共用同一个 root type without erasure/template parameterization。
↡列出所有可访问 element types、驱动 overload 接口生成并决定版本边界的类型序列。generic generation 减少 declaration drift,却没有消除 closed-world assumption:改变 typelist 仍改变 root layout/vtable,并要求 visitors 重新匹配。它自动化 boilerplate,不改变扩展方向。
Back to the “Cyclic” Visitor
官方的 Back to the “Cyclic” Visitor 是在看清 Acyclic trade-off 后重新评估 classic structure。若 element set 确实稳定、每次 traversal 很热、所有 operations 本就应覆盖全部 elements,direct cyclic Visitor 的 virtual overload 更快、更容易静态审计 completeness。
↡Visitor root 明确列出完整 element 集合,elements 与 visitor interface 互相依赖但提供直接双分派的经典结构。“cyclic”描述 dependency graph,不表示 object ownership cycle。forward declarations 和分离 header 可控制 include cycle;ABI/versioning 才是公开 visitor interface 的主要问题。跨 plugin boundary 添加 virtual method 会改变 vtable,不应无版本升级直接部署。
Hooking Variations
Hooking Variations 包括返回值、constness、默认处理、停止 traversal、状态与异常:
Visitor<R>让 Visit 返回统一 R;不同 element 返回不同类型则需 variant/type erasure。visit(const T&)表示只读,mutation visitor 接T&;不要用 const_cast 复用。- catch-all 可 no-op、throw、log 或 delegate base behavior。
- traversal 可由 elements 控制(internal visitor)或 operation 控制(external traversal)。
返回引用需要 lifetime;聚合结果通常保存在 visitor state 或 reducer。异常中止 traversal 时,partially mutated elements 的 consistency 要由 operation 定义。若需要可恢复 transaction,Visitor 只负责 dispatch,rollback/command 协议另建。
Visitor Generic Component Quick Facts
Visitor Generic Component Quick Facts:
- double dispatch 由 Accept + Visit 两次 virtual calls 组成;overload alone 不是动态多分派。
- cyclic Visitor 对稳定 element set/新增 operations 友好;acyclic 对新增 elements/subset operations 更宽松。
- generic typelist generation 减少重复并检查 coverage,但 root 仍依赖 element schema。
- catch-all、return type、constness 与 traversal 必须在 component contract 中固定。
与 variant 和 pattern matching 比较
std::variant<Circle,Polygon> + std::visit 把 element set封闭在 variant type,operation 是 overloaded callable,编译器可检查所有 alternatives,常能 inline;但 recursive/open hierarchy 与 stable ABI 不一定适合。virtual Visitor 对对象 identity、subclass hierarchy 与 separate allocation 更自然。
先预测:AST node types 固定在一个 compiler release,optimization passes 经常增加,classic/generated cyclic Visitor 合适;若 third-party plugins 每月新增 node type,所有旧 passes 不可能知道它,acyclic/default registry 更符合 evolution。
第一步:画出两次分派
从 Base element pointer 和 operation object 出发,逐行标出第一次 accept 的 receiver type、第二次 visit 的 concrete parameter type;验证 overload 不被 hiding。
小结
- Visitor 用 Element::Accept 与 Visitor::Visit 两次 single dispatch 实现 element + operation 双分派
- catch-all 必须进入有效 overload/virtual protocol,function template 本身不能 virtual
- Acyclic Visitor 将每种 element 协议拆开,降低全量依赖,代价是 RTTI 与 unsupported runtime path
- Typelist-generated generic Visitor 自动生成 overload units,但仍由 closed element schema 决定 root interface
- cyclic Visitor 对稳定 elements、频繁新增 operations 与 hot traversal 仍可能是最佳选择
- result、const、fallback、traversal ownership 和 exception 是 Hooking Variations 的正式 contract
- variant/std::visit 提供 closed sum type 的编译期访问;开放 hierarchy 与 ABI 需求可能更适合 virtual Visitor
练习
- 问题 1:手工追踪 double dispatch。
Shape*指向 Circle,Visitor&指向 AreaVisitor;写出两次 virtual receiver 与最终 overload。
- 问题 2:为 plugin elements 选结构。 核心程序不重新编译,plugin 新增 elements,旧 operations 可忽略未知类型;比较 classic 与 acyclic。
- 问题 3:拆分 dispatch 与 traversal。 Scene graph 既要 preorder render 又要 postorder bounds,说明 Accept 不应固定哪种顺序。
名词解释
名词解释
本章出现的专业名词,用大白话再讲一遍。
- double dispatch
- 由 Accept 与 Visit 两步按两个动态类型选择操作的协议。
- Visitor pattern
- 把稳定 element hierarchy 上的可变 operations 抽到独立对象的模式。
- catch-all visitor
- 对未提供更具体 overload 的 element 给出默认处理的后备接口。
- Acyclic Visitor
- 为每个 element 拆独立 Visitor 接口、允许 operation 支持 subset 的结构。
- generic Visitor
- 用类型列表自动生成每 element 访问单元的组件。
- Visitor typelist
- 定义可访问 element schema 并驱动接口生成的类型序列。
- Cyclic Visitor
- root 明确列出全部 elements 并提供直接双分派的经典结构。
- Visitor variations
- result、const、fallback、traversal 与 error 等定制维度。
- Visitor component contract
- 扩展方向、dispatch、coverage、result 与 traversal 的整体约束。
原版目录概念补充核对
以下条目补齐官方目录中容易被示例主线掩盖的概念。它们不重复罗列目录,而是明确每项概念的机制、适用边界和验收证据。
Visitor generic component quick facts:机制、边界与证据
在《第10章:Visitor》的官方单元 mcd-10 中,Visitor generic component quick facts连接本章第 5 组知识约束。学习时要同时说明它接受什么输入、改变什么状态、在何种边界失效;再以本章示例的编译诊断、固定输入输出或失败用例复核结论,不能只记术语名称。