当前位置: 首页 > news >正文

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

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

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

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

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

什么是访问者模式

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


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

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

// 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 是一个非常清晰、性能好的替代方案:它实现的是静态多态 + 运行时安全分发(访问者式的行为,但不需要侵入元素类)。

// 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});// drawfor (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);}// areadouble 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非侵入式 / RTTIAcyclic Visitor
元素类可修改、类型较稳定✅ 很合适(可扩展操作)✅ 可用(若集合闭合)✅ 可用(低侵入)✅(适合模块化)
需要频繁新增操作(算法)✅(访问者友好)❌(需改 visit site)✅(可以外部添加)
性能敏感(避免虚调用)❌ 有虚调用✅ 无虚调用❌ RTTI 成本取决实现
类型在运行时可扩展(插件)❌ 新类型需改 Visitor 接口❌ 变体非动态✅(dynamic_cast 支持)✅(为此设计)
简单、少量类型✅/简单✅ 简洁复杂/过度
http://www.dtcms.com/a/426986.html

相关文章:

  • 哪里可以做网站啊网站上传照片 传不上去
  • 鸿蒙NEXT NearLink Kit入门指南:重新定义短距无线通信
  • 微服务架构:基于Spring Cloud ,构建同城生活服务平台
  • 青岛网站推WordPress主题ao破解版
  • 做网站运营的简历网站开发补充协议 违约
  • Java-Spring入门指南(十三)SpringMVC基本概念与核心流程详解
  • Java Web实战 - 实现用户登录功能
  • 设计模式详解——工厂模式
  • 【大模型】KNighter: 内容审查 漏洞分析
  • WampServer下载安装教程(附安装包,图文并茂)
  • 基于matlab的直流电机调速系统仿真分析-一套
  • MVC 简介
  • c#设计模式—访问者模式
  • 【大数据实战】如何从0到1构建用户画像系统(案例+数据仓库+Airflow调度)
  • 打破数据枷锁:在AWS上解锁Oracle数据库的无限潜能
  • 广州网站推广公司wordpress备份恢复阿里云
  • 不用装专业软件!reaConverter:PSD 转 JPG、PDF 转图片
  • 大模型训练流程及GPU内存解析(110)
  • 学习Python中Selenium模块的基本用法(18:使用ActionChains操作鼠标)
  • 从UI到UE:企业级软件如何做出“高端感”的桌面端界面设计
  • 服务专业的建网站公司电话新站优化案例
  • QCustomPlot 核心功能与图表设置(下)——高级功能实现
  • 莱芜网站排名价格珠海高端网站建设
  • 运营商数据安全的垂直破局:技术适配与场景深耕的双重进化
  • 《Local_Pdf_Chat_RAG 深度学习笔记:PDF 本地化对话的 RAG 原理与实践》
  • Node.js 完全安装与使用指南:Windows 平台详细教程
  • jsp在网站开发中的优势番禺制作网站系统
  • 【Rust GUI开发入门】编写一个本地音乐播放器(5. 制作音乐列表组件)
  • 成都哪家公司做网站比较好h5网站建设机构
  • 少儿舞蹈小程序(20):手机号登录与多角色注册