C++ 访问者模式(Visitor Pattern)
一、模式基础概述
访问者模式属于行为型设计模式,核心思想是将数据结构与数据操作解耦。
在元素类型固定、结构稳定的前提下,可以在不修改原有元素类代码的情况下,灵活地为元素集合新增各类操作;把作用于不同元素的算法逻辑统一抽离到访问者类中,实现职责分离。
核心特点
- 依赖双重分派实现类型自动匹配,避免大量
if-else/switch类型判断 - 结构稳定、操作多变是该模式的最佳使用场景
- 元素负责存储数据,访问者负责执行业务操作
二、核心角色与职责划分
访问者模式共包含五大核心角色,各角色职责、依赖关系如下:
| 角色名称 | 英文标识 | 核心职责 | 关键方法 |
|---|---|---|---|
| 抽象访问者 | Visitor | 定义访问所有具体元素的抽象接口,为每一种具体元素声明对应的访问方法 | visit(具体元素*)纯虚函数 |
| 具体访问者 | ConcreteVisitor | 实现抽象访问者接口,编写针对不同元素的具体业务逻辑 | 重写所有visit方法 |
| 抽象元素 | Element | 定义接收访问者的统一接口,所有具体元素的父类 | accept(Visitor*)纯虚函数 |
| 具体元素 | ConcreteElement | 存储自身业务数据,实现接收访问者的逻辑,触发访问者对应方法 | 重写accept方法 |
| 对象结构 | ObjectStructure | 管理元素集合,提供元素增删、批量遍历能力,对外统一接收访问者 | add()、accept(Visitor*) |
角色依赖关系
- 抽象元素 依赖 抽象访问者(
accept入参为Visitor) - 具体元素 调用 访问者的
visit方法 - 对象结构 持有 多个抽象元素对象,统一调度遍历
- 客户端 组装 对象结构、创建访问者,发起访问动作
三、核心原理:双重分派(Double Dispatch)
双重分派是访问者模式的技术核心,分为两次类型匹配,最终精准执行「访问者+元素」组合对应的逻辑:
- 第一次分派(元素分派)
客户端调用元素对象->accept(访问者),根据元素实际类型执行对应子类的accept方法。 - 第二次分派(访问者分派)
元素内部执行visitor->visit(this),this代表当前具体元素,编译器根据访问者实际类型 + 元素实际类型,匹配并执行对应重载的visit方法。
优势:无需手动判断元素类型,由语言多态自动完成分发,代码更优雅、易维护。
四、完整 C++ 代码实现(带业务示例 + 详细注释)
4.1 基础框架 + 业务场景
示例场景:模拟不同文件类型(文本文件、图片文件),实现两种操作:文件大小统计、文件名称打印。
- 具体元素:文本文件
TextFile、图片文件ImageFile - 具体访问者:大小统计访问者、名称打印访问者
- 对象结构:文件目录(管理所有文件)
#include<iostream>#include<vector>#include<memory>#include<string>// 前向声明:解决循环依赖classTextFile;classImageFile;// ===================== 1. 抽象访问者 Visitor =====================classVisitor{public:// 为每一种具体元素定义访问接口virtualvoidvisit(TextFile*file)=0;virtualvoidvisit(ImageFile*file)=0;// 虚析构:基类指针析构保证安全virtual~Visitor()=default;};// ===================== 2. 抽象元素 Element =====================classFileElement{public:// 接收访问者的抽象接口virtualvoidaccept(Visitor*visitor)=0;virtual~FileElement()=default;// 元素自身数据:文件名、文件大小std::string fileName;longlongfileSize;protected:FileElement(std::string name,longlongsize):fileName(std::move(name)),fileSize(size){}};// ===================== 3. 具体元素 ConcreteElement =====================// 文本文件classTextFile:publicFileElement{public:TextFile(std::string name,longlongsize):FileElement(std::move(name),size){}// 实现accept:触发访问者对应visit方法(第二次分派)voidaccept(Visitor*visitor)override{visitor->visit(this);}};// 图片文件classImageFile:publicFileElement{public:ImageFile(std::string name,longlongsize):FileElement(std::move(name),size){}voidaccept(Visitor*visitor)override{visitor->visit(this);}};// ===================== 4. 具体访问者 ConcreteVisitor =====================// 访问者1:统计所有文件总大小classSizeCountVisitor:publicVisitor{private:longlongtotalSize=0;public:voidvisit(TextFile*file)override{totalSize+=file->fileSize;std::cout<<"统计文本文件:"<<file->fileName<<" 大小:"<<file->fileSize<<"KB\n";}voidvisit(ImageFile*file)override{totalSize+=file->fileSize;std::cout<<"统计图片文件:"<<file->fileName<<" 大小:"<<file->fileSize<<"KB\n";}// 获取统计结果longlonggetTotalSize()const{returntotalSize;}};// 访问者2:打印所有文件名称classNamePrintVisitor:publicVisitor{public:voidvisit(TextFile*file)override{std::cout<<"文本文件名:"<<file->fileName<<"\n";}voidvisit(ImageFile*file)override{std::cout<<"图片文件名:"<<file->fileName<<"\n";}};// ===================== 5. 对象结构 ObjectStructure =====================// 文件目录:管理所有文件元素,提供批量访问能力classFileDirectory{private:// 使用智能指针管理元素生命周期,避免内存泄漏std::vector<std::unique_ptr<FileElement>>fileList;public:// 添加文件元素voidaddFile(std::unique_ptr<FileElement>file){fileList.push_back(std::move(file));}// 批量接收访问者,遍历所有元素voidaccept(Visitor*visitor){for(auto&file:fileList){file->accept(visitor);}}};// ===================== 6. 客户端测试 =====================intmain(){// 1. 构建对象结构:文件目录 + 多个文件FileDirectory directory;directory.addFile(std::make_unique<TextFile>("笔记.txt",128));directory.addFile(std::make_unique<ImageFile>("风景.jpg",2048));directory.addFile(std::make_unique<TextFile>("代码.log",64));// 2. 执行操作1:统计文件大小SizeCountVisitor sizeVisitor;std::cout<<"===== 开始统计文件大小 =====\n";directory.accept(&sizeVisitor);std::cout<<"文件总大小:"<<sizeVisitor.getTotalSize()<<"KB\n\n";// 3. 执行操作2:打印文件名称NamePrintVisitor nameVisitor;std::cout<<"===== 开始打印文件名称 =====\n";directory.accept(&nameVisitor);return0;}4.2 运行输出
===== 开始统计文件大小 ===== 统计文本文件:笔记.txt 大小:128KB 统计图片文件:风景.jpg 大小:2048KB 统计文本文件:代码.log 大小:64KB 文件总大小:2240KB ===== 开始打印文件名称 ===== 文本文件名:笔记.txt 图片文件名:风景.jpg 文本文件名:代码.log五、适用场景与不适用场景
5.1 推荐使用场景
- 对象结构稳定,操作频繁变化
系统中元素类型很少新增,但需要持续新增业务操作(统计、导出、格式化、校验等)。 - 需要对不同元素执行一组关联操作
希望将同类业务逻辑集中管理,分散在各个元素类中会造成代码冗余。 - 需要跨元素聚合数据/状态
例如全局统计、汇总计算、全局遍历分析。 - 典型业务场景
编译器抽象语法树(AST)、文档解析、树形结构遍历、报表生成、日志解析。
5.2 不适用场景
- 元素类型频繁新增
每新增一个具体元素,都需要修改所有抽象访问者、全部具体访问者,破坏开闭原则。 - 操作逻辑简单、元素数量极少
会引入大量抽象类,造成过度设计,增加代码复杂度。 - 元素需要严格封装内部数据
访问者通常需要读取/修改元素内部字段,会破坏元素的封装性。
六、模式优缺点
6.1 优点
- 遵循开闭原则
新增操作只需编写新的访问者类,无需修改原有元素代码。 - 单一职责原则
元素类只负责存储数据,访问者类只负责业务操作,职责划分清晰。 - 操作集中管理
同一套业务逻辑全部放在一个访问者中,便于统一维护、阅读和修改。 - 灵活切换算法
客户端只需替换不同访问者,即可快速切换对元素的操作逻辑。 - 消除类型判断
依靠多态+双重分派,替代臃肿的if-else类型判断。
6.2 缺点
- 新增元素代价极高
新增具体元素时,必须修改抽象访问者接口,并修改每一个已存在的具体访问者,扩展性差。 - 破坏封装性
访问者需要访问元素的内部属性与状态,打破了元素的数据封装。 - 代码复杂度上升
引入多层抽象类、多组重载方法,学习和维护成本高于普通代码。 - 循环依赖风险
元素与访问者互相引用,编码时需要使用前向声明规避编译错误。
七、C++ 工程实践规范与注意事项
- 内存管理
统一使用std::unique_ptr/std::shared_ptr管理元素对象,手动new/delete易造成内存泄漏。 - 虚析构函数
所有抽象基类(Visitor、Element)必须声明虚析构函数,防止基类指针析构子类对象出现内存泄漏。 - 循环依赖处理
元素和访问者互相引用,必须使用类前向声明,不要直接头文件互相包含。 - 接口最小化
元素仅对外提供必要的getter接口,不要暴露全部内部成员,尽量降低对封装的破坏。 - 访问者状态管理
带统计、汇总功能的访问者(如示例中的大小统计)属于有状态访问者,每次使用建议重新实例化,避免状态残留。 - 慎用复杂继承
不要让元素/访问者多层继承叠加,会加剧双重分派的理解难度。
八、模式总结
访问者模式是一把**“偏科型”利器**:
- 优势发挥的前提:元素结构永久稳定,操作持续迭代;
- 核心价值:解耦数据与行为、集中业务逻辑、简化类型判断;
- 使用底线:一旦业务未来可能频繁新增元素类型,坚决不使用该模式。
在 C++ 中结合多态、智能指针、前向声明可以写出健壮、规范的访问者模式代码,常应用于框架底层、解析器、树形结构处理等场景。