C++访问者模式操作元素结构

2026-04-11 22:55:29 723阅读 0评论

访问者模式:让C++结构体“自己开口说它能干啥”

上周帮同事调一个图形渲染模块,他写了七八个形状类(Circle、Rect、Triangle……),每个都得支持序列化、碰撞检测、OpenGL绘制三套逻辑。结果改个坐标系,得翻遍所有类的serialize()collideWith()render()——光是找函数就花了二十分钟。

后来我们把这堆逻辑抽出来,用访问者模式重写。新增一种导出格式?只加一个class SVGExporter : public Visitor就行;加个性能分析器?再写个class ProfilerVisitor,不碰任何形状类一行代码。

这才是访问者模式真正该干的事:让数据结构和算法解耦,让“谁来操作”和“操作谁”彻底分开。


先说清楚它不是什么。
它不是“为了设计模式而设计模式”的玩具。你不会在写个学生管理系统时突然拍桌:“哎呀,该上访问者了!”
它解决的是一类明确的问题

  • 你有一组稳定的元素类型(比如AST节点、UI控件树、几何图元);
  • 但需要频繁添加新的操作(比如语法检查、代码生成、调试打印、内存统计);
  • 而每次加操作都去改每个类的代码,既容易漏,又违反开闭原则。

这时候,访问者就是那个“不请自来却从不乱动你家东西”的客人——它知道怎么敲门(accept()),也知道进门后怎么跟每个主人(具体元素)打招呼(visit(Circle&)visit(Rect&)……),但绝不擅自挪动你家沙发(不修改元素类定义)。


C++里实现它,关键就三步:

第一步:定义访问者接口,穷举所有你能想到的元素类型。
别怕写死——这恰恰是稳定性的体现。比如图形系统里,你明确定义了只有CircleRectPath三种图元,那接口就只列这三个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::variantstd::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>)开始嵌套三层——别硬扛,试试让元素自己开口说:“我认识谁,我能被谁操作。”
那一刻,你就摸到访问者模式的体温了。

文章版权声明:除非注明,否则均为Dark零点博客原创文章,转载或复制请以超链接形式并注明出处。

发表评论

快捷回复: 表情:
验证码
评论列表 (暂无评论,723人围观)

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

目录[+]