C++访问者模式操作元素结构
访问者模式:让C++结构体“自己开口说它能干啥”
上周帮同事调一个图形渲染模块,他写了七八个形状类(Circle、Rect、Triangle……),每个都得支持序列化、碰撞检测、OpenGL绘制三套逻辑。结果改个坐标系,得翻遍所有类的serialize()、collideWith()、render()——光是找函数就花了二十分钟。
后来我们把这堆逻辑抽出来,用访问者模式重写。新增一种导出格式?只加一个class SVGExporter : public Visitor就行;加个性能分析器?再写个class ProfilerVisitor,不碰任何形状类一行代码。
这才是访问者模式真正该干的事:让数据结构和算法解耦,让“谁来操作”和“操作谁”彻底分开。
先说清楚它不是什么。
它不是“为了设计模式而设计模式”的玩具。你不会在写个学生管理系统时突然拍桌:“哎呀,该上访问者了!”
它解决的是一类明确的问题:
- 你有一组稳定的元素类型(比如AST节点、UI控件树、几何图元);
- 但需要频繁添加新的操作(比如语法检查、代码生成、调试打印、内存统计);
- 而每次加操作都去改每个类的代码,既容易漏,又违反开闭原则。
这时候,访问者就是那个“不请自来却从不乱动你家东西”的客人——它知道怎么敲门(accept()),也知道进门后怎么跟每个主人(具体元素)打招呼(visit(Circle&)、visit(Rect&)……),但绝不擅自挪动你家沙发(不修改元素类定义)。
C++里实现它,关键就三步:
第一步:定义访问者接口,穷举所有你能想到的元素类型。
别怕写死——这恰恰是稳定性的体现。比如图形系统里,你明确定义了只有Circle、Rect、Path三种图元,那接口就只列这三个visit重载:
struct Visitor {
virtual void visit(Circle& c) = 0;
virtual void visit(Rect& r) = 0;
virtual void visit(Path& p) = 0;
// 注意:不提供 visit(Shape&)!基类抽象访问会破坏类型精度
};
第二步:所有元素类统一实现 accept(),且只有一行:v.visit(*this);*
这是最常被写错的地方。有人写成v.visit(this),结果调用的是`visit(Shape)`,虚函数表直接跳过具体类型——必须传引用,靠重载决议选中正确函数**。
struct Circle : Shape {
void accept(Visitor& v) override { v.visit(*this); } // 就这一行,但必须对
};
第三步:具体访问者按需实现,只管自己关心的逻辑。
比如导出为JSON:
struct JSONExporter : Visitor {
std::string result;
void visit(Circle& c) override {
result = fmt::format(R"({{"type":"circle","cx":{},"cy":{},"r":{}}})",
c.cx, c.cy, c.r);
}
void visit(Rect& r) override {
result = fmt::format(R"({{"type":"rect","x":{},"y":{},"w":{},"h":{}}})",
r.x, r.y, r.w, r.h);
}
void visit(Path& p) override {
result = R"({"type":"path","data":"..."})"; // 真实实现略
}
};
调用时干净利落:
Circle c{10, 20, 5};
JSONExporter exporter;
c.accept(exporter);
std::cout << exporter.result; // {"type":"circle","cx":10,"cy":20,"r":5}
有人问:C++不是有std::variant和std::visit吗?为什么还要手写?
因为std::visit是“一次性访客”——你得把所有处理逻辑塞进一个lambda里,新增操作就得重写整个lambda。而类形式的访问者天然支持继承:class DebugVisitor : public JSONExporter可以复用JSON逻辑,只重写visit(Rect&)加一行日志。可组合、可复用、可测试,这才是工程场景要的。
还有一点常被忽略:访问者能持有状态。比如做碰撞检测时,CollisionDetector访问者内部存着当前鼠标坐标、世界变换矩阵——这些上下文信息,比在每个Circle::collideWith()里重复传参清爽得多。
最后提醒一个真实踩过的坑:别让访问者反过来持有元素指针。
曾见有人在Visitor里存Shape* currentTarget,然后在visit()里调用currentTarget->doSomething()——这等于又绕回去了。访问者的职责是“处理”,不是“调度”。所有数据应通过参数传入,保持纯函数式风格,单元测试才好写。
访问者模式不是银弹。如果元素类型天天变(今天加Star,明天删Path),它反而增加维护成本。但它在编译器、CAD、游戏引擎这类“结构稳、操作多”的领域,确实是把趁手的刻刀:削繁就简,刀刀落在关节处。
下次当你发现switch (type)散落在七八个文件里,或者if (dynamic_cast<XXX>)开始嵌套三层——别硬扛,试试让元素自己开口说:“我认识谁,我能被谁操作。”
那一刻,你就摸到访问者模式的体温了。


还没有评论,来说两句吧...