第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. 1

    为什么一次 virtual call 不够

    Visitor Basics 解决“操作取决于两个动态类型”:一边是 Shape 实际指向 Circle/Polygon,另一边是 Serialize/Render/Validate operation。C++ virtual dispatch 默认只按 receiver 的动态类型选择;function overload 按参数静态类型选择。

  2. 2

    Visitor 的适用扩展方向

    Visitor 把 operation 从 element classes 中抽出,新增 operation 只需新 Visitor;但新增 element 要给 Visitor base 增 virtual method,并修改所有 concrete visitors。它适合 element hi…

  3. 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。

若 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。

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。

“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。

分步1 / 3

第一步:画出两次分派

从 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. 问题 1:手工追踪 double dispatch。 Shape* 指向 Circle,Visitor& 指向 AreaVisitor;写出两次 virtual receiver 与最终 overload。
  1. 问题 2:为 plugin elements 选结构。 核心程序不重新编译,plugin 新增 elements,旧 operations 可忽略未知类型;比较 classic 与 acyclic。
  1. 问题 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 组知识约束。学习时要同时说明它接受什么输入、改变什么状态、在何种边界失效;再以本章示例的编译诊断、固定输入输出或失败用例复核结论,不能只记术语名称。

资料与写作方式声明

本章以Modern C++ Design, First Edition, Chapter 10: Visitor权威目录界定学习范围,并结合正文列出的技术资料独立重写;不宣称复现原书正文,也不沿用原作表述。

原作版权归作者与出版社所有;本站原创教学结构与表述仅供学习交流。

讨论

评论区加载中…