精读 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 支持) ✅(为此设计)
简单、少量类型 ✅/简单 ✅ 简洁 复杂/过度
相关推荐
lingran__18 小时前
算法沉淀第十天(牛客2025秋季算法编程训练联赛2-基础组 和 奇怪的电梯)
c++·算法
赤月幼狼19 小时前
clickhouse学习笔记(一)基础概念与架构
笔记·学习·clickhouse
消失的旧时光-194319 小时前
kmp需要技能
android·设计模式·kotlin
oioihoii20 小时前
当无符号与有符号整数相遇:C++中的隐式类型转换陷阱
java·开发语言·c++
Yupureki20 小时前
从零开始的C++学习生活 13:红黑树全面解析
c语言·数据结构·c++·学习·visual studio
2401_8762213420 小时前
Euler
c++·数学·算法
AhriProGramming21 小时前
Python学习快速上手文章推荐(持续更新)
开发语言·python·学习·1024程序员节
泡泡鱼(敲代码中)21 小时前
数据结构(顺序表和链表)
笔记·学习·算法
无妄无望21 小时前
在没有网络的环境下安装包pymysql
学习·docker
酌量1 天前
基于3D激光点云的障碍物检测与跟踪---(3)基于匈牙利算法的障碍物跟踪
学习·算法·机器人·匈牙利算法·障碍物跟踪