精读 C++20 设计模式:行为型设计模式 — 访问者模式

精读 C++20 设计模式:行为型设计模式 --- 访问者模式

​ 访问者模式是另一个经典的设计模式------它把"算法"与"数据结构"分离:把作用于一组对象的操作从对象中抽离出来,以便在不修改这些对象类的情况下添加新的操作。

​ 这里会涉及到分发这个事情:分发其实就是一组判断逻辑执行后执行不用的结果函数(运行时决定调用哪个函数实现的机制),常见的有单分发(single dispatch):只有调用对象的动态类型用于选择函数(例如常见的虚函数);和双分发(double dispatch):函数选择依赖两个对象的运行时类型(访问者 + 元素)。

​ 访问者模式常用来实现双分发:元素把 this 传给访问者,从而同时根据元素类型和访问者类型采取不同行为。

什么是访问者模式

访问者模式把要在一组元素上执行的操作提取到一个独立的访问者(Visitor)对象中。每个元素类实现 accept(Visitor&),并在其中调用 visitor.visit(*this) ------ 这样访问者就能根据元素具体类型执行不同逻辑


侵入式访问者(经典实现) --- 聊天室里的图形元素 Demo

这个示例展示经典、侵入式访问者:元素(Element)提供 accept(Visitor&),访问者声明针对每个具体元素的 visit 重载。示例用一个简单图形元素集合(CircleRectangle)并实现两个访问者:DrawVisitor(绘制)与 AreaVisitor(计算面积)。

cpp 复制代码
// visitor_classic.cpp --- 经典访问者示例(可直接编译)
#include <iostream>
#include <memory>
#include <vector>
#include <cmath>

// 前向声明具体元素
struct Circle;
struct Rectangle;

// 访问者接口:每新增一个具体元素,就要在这里添加一个 visit(C&) 函数
struct Visitor {
    virtual ~Visitor() = default;
    virtual void visit(const Circle& c) = 0;
    virtual void visit(const Rectangle& r) = 0;
};

// 元素接口(Element)
struct Shape {
    virtual ~Shape() = default;
    virtual void accept(Visitor& v) const = 0;
};

// 具体元素:Circle
struct Circle : Shape {
    double x,y, r;
    Circle(double x_, double y_, double r_) : x(x_), y(y_), r(r_) {}
    void accept(Visitor& v) const override { v.visit(*this); }
};

// 具体元素:Rectangle
struct Rectangle : Shape {
    double x,y,w,h;
    Rectangle(double x_, double y_, double w_, double h_) : x(x_), y(y_), w(w_), h(h_) {}
    void accept(Visitor& v) const override { v.visit(*this); }
};

// 一个绘制访问者(打印示意)
struct DrawVisitor : Visitor {
    void visit(const Circle& c) override {
        std::cout << "Draw Circle at (" << c.x << "," << c.y << ") r=" << c.r << "\n";
    }
    void visit(const Rectangle& r) override {
        std::cout << "Draw Rectangle at (" << r.x << "," << r.y << ") w=" << r.w << " h=" << r.h << "\n";
    }
};

// 一个计算面积的访问者
struct AreaVisitor : Visitor {
    double total = 0.0;
    void visit(const Circle& c) override {
        total += M_PI * c.r * c.r;
    }
    void visit(const Rectangle& r) override {
        total += r.w * r.h;
    }
};

int main() {
    std::vector<std::unique_ptr<Shape>> shapes;
    shapes.push_back(std::make_unique<Circle>(0,0,1));
    shapes.push_back(std::make_unique<Rectangle>(0,0,2,3));
    shapes.push_back(std::make_unique<Circle>(1,1,2));

    DrawVisitor draw;
    for (auto& s : shapes) s->accept(draw);

    AreaVisitor area;
    for (auto& s : shapes) s->accept(area);
    std::cout << "Total area: " << area.total << "\n";
    return 0;
}

Visitor要注册对应的 visit(const Concrete&) ,其对应一个具体元素类型。它是开放给"新增操作"的扩展点。

​ Shape的Accept接口让元素把自己"交给"访问者------这一步实现了双分发 :运行时元素类型决定 accept 的行为(虚函数),而 accept 内再调用 visitor.visit(*this),访问者根据元素静态类型选择正确的 visit 重载(编译时静态多态实际上在这里由函数签名+虚函数协作完成)。

​ 要添加新元素类型 ,需要修改 Visitor 接口和所有现有访问者(侵入式缺点)。而添加新操作(即新的访问者类)不需要改元素类(这是访问者模式的优点)。


访问者的几个常见变种(含样例与说明)

访问者有很多变种,工程中常见的包括:

  1. 经典(侵入式)访问者 (上面已展示)
    • 优点:对新增操作友好(只需新增 Visitor)。
    • 缺点:对新增元素类型(Concrete Element)不友好,需要修改 Visitor 接口及所有 Visitor 实现。
  2. Acyclic Visitor(无环访问者)
    • 目标是降低对 Visitor 接口的集中修改:用每个元素定义自己专属的 visitor 接口(例如 CircleVisitableRectangleVisitable),访问者根据是否实现某个接口来决定是否访问 ------ 常借助 RTTI / dynamic_cast。这能避免集中修改一个超大的 Visitor 接口,但增加了接口数量与复杂度。
    • 代码风格较多见于以插件/模块化为目的的系统。
  3. 非侵入式访问者(外部分发)
    • 不要求元素实现 accept,而通过外部分发器使用 RTTI(dynamic_cast)或 typeid 来识别元素真实类型并调用相应操作。实现简单,但性能与类型安全有所折中。
    • 适合无法修改元素类(第三方库类型)但仍想"访问"其具体类型的场景。
  4. 基于 std::variant + std::visit 的访问者(数据型替代)
    • 如果元素集合是封闭(disjoint union),std::variant + std::visit 提供了一个类型安全、非侵入式、无虚函数的替代方案。下面会给出完整示例。
    • 优点:编译期类型安全、没有虚表开销、对新增变体需要修改 std::variant 类型列表(和新增元素一样的限制)。
    • 缺点:变体必须是闭合的(编译时已知的类型集合),不适合运行时会动态新增元素类型的插件式场景。
  5. 返回值 / 结果型访问者
    • 访问者除了"访问"外可以返回结果(例如 AreaVisitor 累计面积返回 double)。实现时 visit 可以返回值或通过成员变量传回结果。
    • 在多并发/不可变场景,推荐用返回值而非内部可变状态以便更好组合与测试。

std::variant + std::visit:现代 C++ 的替代思路

当元素集合是闭合 的(即所有可能类型在编译时已知),std::variant + std::visit 是一个非常清晰、性能好的替代方案:它实现的是静态多态 + 运行时安全分发(访问者式的行为,但不需要侵入元素类)。

cpp 复制代码
// variant_visit.cpp
#include <iostream>
#include <variant>
#include <vector>
#include <cmath>

struct Circle { double x,y,r; };
struct Rectangle { double x,y,w,h; };

using Shape = std::variant<Circle, Rectangle>;

// helper overload for visit
template<class... Ts> struct Overloaded : Ts... { using Ts::operator()...; };
template<class... Ts> Overloaded(Ts...) -> Overloaded<Ts...>;

int main() {
    std::vector<Shape> shapes;
    shapes.push_back(Circle{0,0,1});
    shapes.push_back(Rectangle{0,0,2,3});
    shapes.push_back(Circle{1,1,2});

    // draw
    for (auto& s : shapes) {
        std::visit(Overloaded{
            [](const Circle& c){ std::cout << "Draw Circle r=" << c.r << "\n"; },
            [](const Rectangle& r){ std::cout << "Draw Rect w=" << r.w << " h=" << r.h << "\n"; }
        }, s);
    }

    // area
    double total = 0;
    for (auto& s : shapes) {
        total += std::visit(Overloaded{
            [](const Circle& c)->double { return M_PI * c.r * c.r; },
            [](const Rectangle& r)->double { return r.w * r.h; }
        }, s);
    }
    std::cout << "Total area: " << total << "\n";
    return 0;
}

std::variant + std::visit 这个方案好在哪?首先他没有虚表开销(更适合高性能场景),编译器检查 std::visit 覆盖了变体里的每个类型(或你可以提供默认)。所有访问逻辑放在 std::visit 的重载组内(便于阅读)。

总结

问题

  • 在一组不同类型对象上执行多种不相关的操作,如果把操作代码散落到元素类中,类会臃肿;而需要在不修改元素类的前提下新增操作(或新增元素),这导致设计权衡。

如何解决

  • 访问者模式把操作(算法)抽离成访问者类,元素提供 accept 让访问者对元素进行"访问" ------ 从而实现双分发(访问者 + 元素的运行时类型共同决定执行行为)。
  • 在现代 C++ 中,根据闭合性与性能需求,也可以用 std::variant + std::visit 来实现类似访问者的效果,但以"数据联合"的方式(非基于继承)。

优点

  • 新增操作非常方便 (对经典访问者):只需添加一个新的 Visitor,元素类不变。
  • 逻辑集中:把复杂操作集中到访问者里,元素职责保持单一。
  • 双分发:可以根据访问者类型和元素类型同时做出决策(这个是很多模式难以直接实现的)。

缺点

  • 侵入式接口 :经典访问者需要在 Visitor 上为每个具体元素添加 visit,因此 新增元素类成本高(需要改动 Visitor 接口与所有 Visitor 实现)。
  • 可扩展性权衡:如果需要两方面都经常扩展(既常新增元素又常新增操作),访问者并非全能解,要在架构上做权衡(拆域、拆接口、或选 variant + visit)。
  • 实现复杂度:一些变体(Acyclic、Reflection、表驱动)更灵活但实现成本更高。
场景 / 需求 侵入式访问者 std::variant + visit 非侵入式 / RTTI Acyclic Visitor
元素类可修改、类型较稳定 ✅ 很合适(可扩展操作) ✅ 可用(若集合闭合) ✅ 可用(低侵入) ✅(适合模块化)
需要频繁新增操作(算法) ✅(访问者友好) ❌(需改 visit site) ✅(可以外部添加)
性能敏感(避免虚调用) ❌ 有虚调用 ✅ 无虚调用 ❌ RTTI 成本 取决实现
类型在运行时可扩展(插件) ❌ 新类型需改 Visitor 接口 ❌ 变体非动态 ✅(dynamic_cast 支持) ✅(为此设计)
简单、少量类型 ✅/简单 ✅ 简洁 复杂/过度
相关推荐
长路归期无望3 小时前
C语言小白实现多功能计算器的艰难历程
c语言·开发语言·数据结构·笔记·学习·算法
知识分享小能手3 小时前
微信小程序入门学习教程,从入门到精通,微信小程序常用API(上)——知识点详解 + 案例实战(4)
前端·javascript·学习·微信小程序·小程序·html5·微信开放平台
linux kernel5 小时前
第二十三讲:特殊类和类型转换
开发语言·c++
渡我白衣5 小时前
深入剖析:boost::intrusive_ptr 与 std::shared_ptr 的性能边界和实现哲学
开发语言·c++·spring
yuxb735 小时前
Ceph 分布式存储学习笔记(二):池管理、认证和授权管理与集群配置(下)
笔记·ceph·学习
怀旧,5 小时前
【C++】26. 智能指针
开发语言·c++·算法
我真的是大笨蛋5 小时前
依赖倒置原则(DIP)
java·设计模式·性能优化·依赖倒置原则·设计规范
悠哉悠哉愿意5 小时前
【ROS2学习笔记】话题通信篇:python话题订阅与发布
笔记·学习·ros2
bruk_spp6 小时前
从pty驱动学习tty设备驱动加载
linux·学习