自学内容网 自学内容网

【读书笔记】《C++ Software Design》第四章:The Visitor Design Pattern

《C++ Software Design》第四章:The Visitor Design Pattern

本章通过对比多种实现方式(面向过程、动态多态、std::variant 及 acyclic visitor),深入探讨如何在“类型扩展”与“操作扩展”之间进行权衡,并给出实用建议。

  • 经典 Visitor 模式 UML 图
     +----------------+         +--------------------+
     |    Visitor     |<--------|   ConcreteVisitor  |
     +----------------+         +--------------------+
     | +visit(Circle) |         | +visit(Circle)     |
     | +visit(Square) |         | +visit(Square)     |
     +----------------+         +--------------------+

            ▲                            ▲
            |                            |
    +----------------+        +------------------+
    |     Shape      |<-------|  ConcreteShape   |
    +----------------+        +------------------+
    | +accept(Visitor)|       | +accept(Visitor) |
    +----------------+        +------------------+
  • Shape 为抽象元素,定义 accept()
  • ConcreteShape 实现 accept() 并调用 Visitor.visit(this)
  • Visitor 定义所有 visit() 函数。
  • ConcreteVisitor 提供操作的实现(如 Draw、Area)。

Guideline 15: Design for the Addition of Types or Operations

本节关注“在已有数据结构上增加新操作”的设计问题 —— 典型的开放-封闭冲突问题。

15.1 A Procedural Solution

  • 描述:最直接的方式是使用 switch-caseif-else 语句基于类型进行分发。

  • 示例

    enum class ShapeType { Circle, Square };
    
    struct Shape {
        ShapeType type;
        // union or polymorphic data omitted
    };
    
    void draw(const Shape& s) {
        switch (s.type) {
            case ShapeType::Circle: /* draw circle */ break;
            case ShapeType::Square: /* draw square */ break;
        }
    }
    
  • 问题

    • 易于添加“新操作”(如 drawarea),但添加“新类型”则需修改所有 switch
    • 不符合开闭原则(Open-Closed Principle)。

15.2 An Object-Oriented Solution

  • 策略:使用继承 + 虚函数封装行为,遵循 OCP。

  • 示例

    struct Shape {
        virtual void draw() const = 0;
        virtual ~Shape() = default;
    };
    
    struct Circle : Shape {
        void draw() const override { /* ... */ }
    };
    
    struct Square : Shape {
        void draw() const override { /* ... */ }
    };
    
  • 利点:添加新类型(如 Triangle)无需改动旧代码。

  • 缺点:若要添加新操作(如 area()),则需修改所有类,不利于“操作的扩展”。

15.3 Be Aware of the Design Choice in Dynamic Polymorphism

  • 选择权衡

    • 如果“类型稳定,操作频繁变化”,应选择 Visitor。
    • 如果“类型频繁变化”,应使用虚函数或策略模式。

Guideline 16: Use Visitor to Extend Operations

16.1 Analyzing the Design Issues

  • 核心问题:如何在不修改类定义的前提下添加新行为?
  • 背景:传统的虚函数机制“封闭类型、开放行为”——Visitor 颠倒了这一点。

16.2 The Visitor Design Pattern Explained

  • 结构

    struct Visitor {
        virtual void visit(const Circle&) = 0;
        virtual void visit(const Square&) = 0;
    };
    
    struct Shape {
        virtual void accept(Visitor&) const = 0;
    };
    
    struct Circle : Shape {
        void accept(Visitor& v) const override { v.visit(*this); }
    };
    
    struct DrawVisitor : Visitor {
        void visit(const Circle& c) override { /* draw */ }
        void visit(const Square& s) override { /* draw */ }
    };
    
  • 优点

    • 可在不改动原有类型的前提下添加操作。
    • 非侵入式操作统一分发逻辑。
  • 典型应用:编译器 AST、图形系统、财务数据处理。

16.3 Analyzing the Shortcomings of the Visitor Design Pattern

  • 问题

    1. 添加新类型需要修改所有 Visitor(不满足类型扩展开放)。
    2. 类型之间强依赖(visit() 参数硬编码)。
    3. 类型与 Visitor 紧耦合(accept() 强制内嵌)。
    4. RTTI 方案难以替代时支持 const, non-const, mutable 等情况繁琐。
  • 改进方向:尝试 std::variant 或基于 CRTP/模板的 Visitor。


Guideline 17: Consider std::variant for Implementing Visitor

C++17 起,std::variant 提供了一个值语义的、类型安全的 sum type,可以替代部分 Visitor 功能。

17.1 Introduction to std::variant

std::variant 实现结构图

            +----------------------------+
            |       std::variant        |
            |   <Circle, Square, ...>   |
            +----------------------------+
                       |
                       v
         +----------------------------+
         |       std::visit()         |
         |    overloaded visitors     |
         +----------------------------+

说明:

  • std::variant 使用值语义,避免继承与虚函数。

  • 通过 std::visit() 进行访问,支持编译期多态。

  • 每个操作通过 lambda 或 std::function 实现,无需嵌入原类型。

  • 示例

    using Shape = std::variant<Circle, Square>;
    
    struct Draw {
        void operator()(const Circle&) const { /* draw circle */ }
        void operator()(const Square&) const { /* draw square */ }
    };
    
    void draw(const Shape& s) {
        std::visit(Draw{}, s);
    }
    
  • 核心优势:\n

    • 无需基类,无需虚函数。
    • 值语义,结构更扁平、组合灵活。

17.2 Refactoring the Drawing of Shapes as a Value-Based, Nonintrusive Solution

  • 非侵入性:类型本身无需知道 Visitor 的存在(无 accept() 方法)。
  • 组合性:多个 std::variant 可嵌套或组合,便于构造更复杂的数据结构。

17.3 Performance Benchmarks

  • 动态多态 vs std::variant

    • 虚函数:动态分派,运行时效率高于 naive RTTI,但无法内联。
    • variant:分派由 std::visit() 完成,可被编译器优化(如内联、多态擦除)。
    • 在某些场景下,variant 更适合数据密集型、计算密集型任务(如图形渲染管线)。

17.4 Analyzing the Shortcomings of the std::variant Solution

  • 劣势

    1. 类型必须在编译时列举所有可能性(不适用于运行时可扩展系统)。
    2. std::visit 的嵌套 lambda 可读性差,维护成本高。
    3. 类似 CRTP 结构时组合受限。
  • 建议

    • 若类型集合固定,推荐使用 variant
    • 若需插件或运行时多态,仍推荐 Visitor 或策略模式。

Guideline 18: Beware the Performance of Acyclic Visitor

  • 介绍

    • Acyclic Visitor 是一种更灵活的 Visitor 变体(支持运行时扩展)。
    • 每个 visit() 调用由 RTTI+dynamic_cast 支持,避免了强耦合。
  • 实现方式

    struct BaseVisitor {
        virtual ~BaseVisitor() = default;
    };
    
    struct Circle;
    struct Square;
    
    struct VisitorForCircle {
        virtual void visit(const Circle&) = 0;
    };
    
    struct Circle : Shape {
        void accept(BaseVisitor& v) const override {
            if (auto* vc = dynamic_cast<VisitorForCircle*>(&v)) {
                vc->visit(*this);
            }
        }
    };
    
  • 性能问题

    • 每个 accept() 都可能包含多个 dynamic_cast,分支判断复杂,导致性能下降。
  • 建议

    • 限制 acyclic visitor 用于插件系统或需热扩展场景;
    • 普通业务逻辑仍应首选静态 Visitor 或 std::variant

小结

指导原则核心思想推荐场景
Guideline 15明确类型/操作变化频率,选择动态多态或静态调度所有领域
Guideline 16使用 Visitor 实现“操作的开放性”AST、图形系统
Guideline 17std::variant 实现值语义、非侵入式 Visitor类型固定系统、现代 C++ 项目
Guideline 18谨慎使用 acyclic visitor,关注性能插件架构、运行时类型系统

已为第四章《The Visitor Design Pattern》补充完整展开内容,包括每条 Guideline 的背景动机、典型实现、优缺点分析以及适用建议。内容涵盖:

  • 面向过程 vs 面向对象 vs Visitor 的对比
  • std::variant 的现代替代方案
  • acyclic visitor 的结构与性能警示
  • 各种设计在 C++ 实践中的具体实现形式

延伸

C++ 模板版 Visitor(静态多态 CRTP)

适用于类型固定、操作多变的场景(完全去虚函数,纯编译期类型分派):

// Step 1: 定义所有可访问的类型
struct Circle { void draw() const { std::cout << "Draw Circle\n"; } };
struct Square { void draw() const { std::cout << "Draw Square\n"; } };

// Step 2: 定义泛型访问器接口
template<typename T>
struct StaticVisitor {
    void visit(const T& t) const {
        static_cast<const T&>(t).draw();  // 默认调用 draw() 方法
    }
};

// Step 3: 类型持有容器 + 统一 dispatch
template<typename... Ts>
class Variant {
    std::variant<Ts...> data_;
public:
    Variant(std::variant<Ts...> d): data_(std::move(d)) {}

    template<typename Visitor>
    void accept(Visitor&& v) const {
        std::visit([&v](const auto& obj){ v.visit(obj); }, data_);
    }
};

// 使用:
int main() {
    using MyShape = Variant<Circle, Square>;
    MyShape shape1{ Circle{} };
    MyShape shape2{ Square{} };

    StaticVisitor<Circle> visitor;
    shape1.accept(visitor);  // 输出 Draw Circle
    shape2.accept(visitor);  // 输出 Draw Square
}

✅ 优点:零运行时开销,完全可内联,可用于编译期组合行为(元编程支持)。


进阶模板技巧:Overloaded Helper

用于组合多个 lambda 表达式以实现静态多态访问:

template<class... Ts>
struct Overloaded : Ts... { using Ts::operator()...; };
template<class... Ts> Overloaded(Ts...) -> Overloaded<Ts...>;

std::variant<Circle, Square> shape = Circle{};
std::visit(Overloaded{
    [](const Circle& c){ c.draw(); },
    [](const Square& s){ s.draw(); }
}, shape);

Summary

Visitor 实现方式对比

方案类型扩展性操作扩展性值语义性能易用性备注
面向对象 + 虚函数高(动态分派)常规方法
Visitor 模式中(visit调用)经典方案
Acyclic Visitor低(RTTI)插件化结构
std::variant高(编译期)现代推荐
模板版静态 Visitor最高(全静态)低(需模板)高级泛型编程
场景需求推荐模式
类型固定、操作多变Visitor / variant
操作固定、类型频繁变化继承+虚函数
插件系统、运行时扩展Acyclic Visitor
性能敏感、低耦合、高重用模板静态 Visitor
面向值语义,使用现代 C++ 特性std::variant

原文地址:https://blog.csdn.net/YZJincsdn/article/details/149306618

免责声明:本站文章内容转载自网络资源,如侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!