
1. 访问者模式一个被误解的“复杂”模式在C社区里提到访问者模式很多人的第一反应是“复杂”、“难懂”、“过度设计”。我最初接触它时也抱有这样的偏见觉得它那套双分派Double Dispatch的机制绕来绕去远不如直接写个if-else或者虚函数来得直接痛快。直到我在一个图形编辑器的渲染和导出模块上栽了跟头才彻底改变了对这个模式的看法。那个编辑器需要支持多种图形元素Shape比如圆形Circle、矩形Rectangle、三角形Triangle。最初的需求很简单把所有图形渲染到屏幕上。我设计了一个Shape基类带一个virtual void draw() const 0;的纯虚函数各个子类实现自己的绘制逻辑。代码清晰运行良好我觉得自己设计得挺优雅。问题出在需求变更上。产品经理说“我们需要把整个画布导出为SVG格式。” 没问题我在Shape基类里加了一个virtual void exportToSVG(std::ostream os) const 0;。没过多久新需求又来了“还要支持导出为PNG图片的元数据描述。” 于是我又加了一个virtual void exportToPNGMeta(...) const 0;。紧接着“需要计算所有图形的总面积”、“需要生成JSON格式的序列化”、“需要高亮显示被选中的图形”……每一个新操作都意味着我要回到Shape基类添加一个新的纯虚函数然后跑到每一个具体的Circle、Rectangle、Triangle类里去实现这个方法。灾难开始了。Shape基类变得极其臃肿它不再是一个稳定的、描述图形本质的抽象而成了一个会因为各种外部操作需求而频繁变动的“大杂烩”。更糟糕的是每增加一个图形类型比如后来加的Ellipse我就必须为它实现所有的操作方法哪怕其中某些操作对这个新图形暂时没有意义。代码的耦合度急剧上升Shape类族的稳定性和可维护性荡然无存。这就是典型的“增加操作困难”场景——当你的对象结构图形类型相对稳定但需要在其上定义的操作却可能频繁增加和变化时访问者模式的价值就凸显出来了。访问者模式的核心思想正是将数据结构与作用于结构上的操作分离。它允许你在不修改现有类层次结构的前提下定义新的操作。听起来很抽象我们可以把它想象成去医院体检。你的身体对象结构是相对稳定的由心、肝、脾、肺、肾等器官具体元素类组成。而体检项目操作则是多变的今天来个常规检查GeneralCheckupVisitor明天做个专项筛查CancerScreeningVisitor。访问者模式就像派不同的医生访问者来“访问”你的各个器官。每个医生都懂得一套完整的检查方法visitHeart,visitLiver...你的每个器官只需要接受访问提供一个accept(Visitor)接口至于具体被怎么“检查”那是医生访问者的事。这样增加新的体检项目操作只需要培训一位新医生实现一个新的访问者类完全不需要在你的身体对象结构上动刀子。在C中实现访问者模式尤其是要优雅地处理起来有几个绕不开的坎如何实现双分派如何处理返回值如何利用C11/14/17的现代特性让代码更安全、更简洁这篇文章我就结合自己重构那个图形编辑器的实战经历手把手带你用现代CC11为核心实现一个类型安全、扩展性强的访问者模式并深入探讨其中的设计抉择、常见陷阱和性能考量。2. 模式核心机制与C实现难点剖析在深入代码之前我们必须彻底理解访问者模式运转的两个核心机制双分派Double Dispatch和稳定的元素接口。这是理解后续所有设计选择的基石。2.1 双分派为什么普通的虚函数不够用让我们回到最初的图形例子。假设我们只有draw操作用虚函数实现很简单class Shape { public: virtual void draw() const 0; virtual ~Shape() default; }; class Circle : public Shape { public: void draw() const override { std::cout Drawing a Circle\n; } }; class Rectangle : public Shape { public: void draw() const override { std::cout Drawing a Rectangle\n; } };这里发生的是单分派调用哪个draw方法只取决于调用者一个Shape*指针的动态类型是Circle还是Rectangle。编译器在运行时通过虚表vtable查找正确的函数地址。现在考虑一个“碰撞检测”操作它需要知道两个图形具体是什么类型才能计算。伪代码逻辑可能是if (shape1 is Circle shape2 is Circle) { 计算圆与圆碰撞 } else if (shape1 is Circle shape2 is Rectangle) { 计算圆与矩形碰撞 } ...。如果用虚函数你可能会想在Shape里加一个virtual bool collidesWith(const Shape other) const 0;。但在每个子类里实现这个方法时你都会遇到一个问题other的具体类型是什么你不得不使用dynamic_cast进行一连串的类型判断代码会退化成丑陋的“类型开关”丧失了面向对象的多态优势。bool Circle::collidesWith(const Shape other) const { if (auto circle dynamic_castconst Circle*(other)) { // 圆与圆碰撞计算 return calculateCircleCircleCollision(*this, *circle); } else if (auto rect dynamic_castconst Rectangle*(other)) { // 圆与矩形碰撞计算 return calculateCircleRectCollision(*this, *rect); } // ... 更多类型判断 throw std::runtime_error(Unsupported shape type for collision); }这就是双分派要解决的问题让函数调用的行为同时依赖于两个对象的实际类型这里是shape1和shape2。访问者模式通过两次虚函数调用来模拟这一点第一次分派元素如Circle的accept(Visitor)方法被调用这确定了第一个对象的类型。第二次分派在accept方法内部调用visitor.visitCircle(*this)这确定了操作访问者的类型。至此调用者Circle和参数Visitor的具体子类的类型都确定了。2.2 元素类的稳定接口accept方法的必要性访问者模式要求每个元素类如Circle,Rectangle实现一个accept方法。这是整个模式得以运转的“协议”。accept方法通常非常简单class Circle : public Shape { public: // ... 其他成员 void accept(ShapeVisitor visitor) override { visitor.visitCircle(*this); // 关键将自身Circle传递给访问者 } };这个简单的调用是精妙之处。它把元素对象自身的引用*this作为参数传递给了访问者接口中对应的、以该元素类型为参数的visit方法如visitCircle(Circle)。这样在visitCircle方法内部访问者就获得了具体类型为Circle的对象引用可以安全地调用Circle特有的方法比如getRadius()而无需任何dynamic_cast。这里引出了C实现的第一难点访问者接口的设计。我们需要一个ShapeVisitor类它为每一种具体的元素类型声明一个visit方法。class ShapeVisitor { public: virtual ~ShapeVisitor() default; virtual void visitCircle(Circle circle) 0; virtual void visitRectangle(Rectangle rect) 0; virtual void visitTriangle(Triangle tri) 0; };看到问题了吗这个接口依赖于所有具体元素类的前置声明或完整定义。这导致了编译期的循环依赖ShapeVisitor需要知道Circle,Rectangle等类的存在而这些具体类在实现accept时又需要包含ShapeVisitor的头文件。如果处理不当会导致头文件包含混乱。通常的解决方法是使用前置声明并在一个独立的头文件里集中管理这些声明。2.3 返回值处理与常量正确性我们的DrawVisitor可能没有返回值但AreaVisitor需要返回doubleExportToJsonVisitor可能需要返回一个std::string。如何在访问者接口中统一处理返回值一种简单粗暴的方法是将返回值作为访问者对象的成员变量。例如AreaVisitor在遍历过程中累加面积最后通过一个成员函数getTotalArea()返回结果。这种方式对void和non-void返回值都适用但不够函数式且访问者对象可能因为保存状态而无法被const修饰。另一种更优雅、更现代的方法是利用C的模板和类型推导让每个visit方法可以有不同的返回类型。但这会使得访问者接口变成一个模板类增加了复杂性。我们会在后续的进阶实现中探讨这种方法。此外还有常量正确性问题。如果一个操作如计算面积、导出不应该修改元素对象那么accept方法和visit方法都应该接受const引用。这会衍生出ConstShapeVisitor接口和const版本的accept方法。在实际项目中我建议默认提供const版本除非操作确实需要修改元素。这更符合“查询与修改分离”的原则。3. 基础实现从经典结构到可编译的代码理解了核心机制后我们开始搭建一个最基础的、可运行的访问者模式框架。我们将以图形编辑器为例实现一个DrawVisitor和一个AreaVisitor。3.1 定义元素类层次结构首先我们定义稳定的元素基类Shape和具体的图形类。为了处理头文件依赖我们通常将访问者基类的前置声明放在这里。shape.h#ifndef SHAPE_H #define SHAPE_H // 前置声明访问者类 class ShapeVisitor; class ConstShapeVisitor; // 元素基类 class Shape { public: virtual ~Shape() default; // 接受非常量访问者 virtual void accept(ShapeVisitor visitor) 0; // 接受常量访问者 virtual void accept(ConstShapeVisitor visitor) const 0; }; // 具体元素类声明 class Circle; class Rectangle; class Triangle; #endif // SHAPE_H接下来在一个单独的头文件例如shape_visitor.h中定义访问者接口。这里集中管理对所有具体元素类的前置声明。shape_visitor.h#ifndef SHAPE_VISITOR_H #define SHAPE_VISITOR_H // 集中前置声明所有具体元素类避免在多个地方重复声明 class Circle; class Rectangle; class Triangle; // 非常量访问者接口 class ShapeVisitor { public: virtual ~ShapeVisitor() default; virtual void visitCircle(Circle circle) 0; virtual void visitRectangle(Rectangle rect) 0; virtual void visitTriangle(Triangle tri) 0; }; // 常量访问者接口 class ConstShapeVisitor { public: virtual ~ConstShapeVisitor() default; virtual void visitCircle(const Circle circle) 0; virtual void visitRectangle(const Rectangle rect) 0; virtual void visitTriangle(const Triangle tri) 0; }; #endif // SHAPE_VISITOR_H现在我们可以实现具体的元素类了。以Circle为例circle.h#ifndef CIRCLE_H #define CIRCLE_H #include shape.h #include shape_visitor.h // 需要访问者接口的完整定义来实现accept #include cmath class Circle : public Shape { private: double radius_; double centerX_, centerY_; public: Circle(double r, double x 0.0, double y 0.0) : radius_(r), centerX_(x), centerY_(y) { if (r 0.0) throw std::invalid_argument(Radius must be non-negative); } double getRadius() const { return radius_; } std::pairdouble, double getCenter() const { return {centerX_, centerY_}; } // 实现accept方法 void accept(ShapeVisitor visitor) override { visitor.visitCircle(*this); } void accept(ConstShapeVisitor visitor) const override { visitor.visitCircle(*this); } }; #endif // CIRCLE_HRectangle和Triangle的实现类似它们各自有自己的属性如宽高、顶点坐标和对应的accept实现。3.2 实现具体的访问者访问者的实现就直观多了。每个访问者对应一种操作。我们先实现一个无返回值的DrawVisitordraw_visitor.h#ifndef DRAW_VISITOR_H #define DRAW_VISITOR_H #include shape_visitor.h #include iostream class DrawVisitor : public ShapeVisitor { public: void visitCircle(Circle circle) override { auto [x, y] circle.getCenter(); std::cout [Draw] Circle at ( x , y ) with radius circle.getRadius() std::endl; } void visitRectangle(Rectangle rect) override { auto [w, h] rect.getDimensions(); auto [x, y] rect.getTopLeft(); std::cout [Draw] Rectangle at ( x , y ) with width w and height h std::endl; } void visitTriangle(Triangle tri) override { auto points tri.getVertices(); std::cout [Draw] Triangle with vertices at ( points[0].first , points[0].second ), ( points[1].first , points[1].second ), ( points[2].first , points[2].second ) std::endl; } }; #endif // DRAW_VISITOR_H再实现一个有返回值的AreaVisitor。我们采用将结果存储在访问者对象内部的方式area_visitor.h#ifndef AREA_VISITOR_H #define AREA_VISITOR_H #include shape_visitor.h class AreaVisitor : public ConstShapeVisitor { // 注意继承自ConstShapeVisitor因为计算面积不修改对象 private: double totalArea_ 0.0; public: void visitCircle(const Circle circle) override { double area M_PI * circle.getRadius() * circle.getRadius(); totalArea_ area; // 可以在这里打印调试信息 // std::cout Adding circle area: area std::endl; } void visitRectangle(const Rectangle rect) override { auto [w, h] rect.getDimensions(); totalArea_ w * h; } void visitTriangle(const Triangle tri) override { // 假设使用顶点坐标计算三角形面积如鞋带公式 auto v tri.getVertices(); double area 0.5 * std::abs( (v[0].first - v[2].first) * (v[1].second - v[0].second) - (v[0].first - v[1].first) * (v[2].second - v[0].second) ); totalArea_ area; } double getTotalArea() const { return totalArea_; } void reset() { totalArea_ 0.0; } }; #endif // AREA_VISITOR_H3.3 组合使用客户端代码示例现在我们可以在main函数中看到访问者模式如何优雅地工作#include vector #include memory #include circle.h #include rectangle.h #include triangle.h #include draw_visitor.h #include area_visitor.h int main() { std::vectorstd::unique_ptrShape shapes; shapes.push_back(std::make_uniqueCircle(5.0, 1.0, 2.0)); shapes.push_back(std::make_uniqueRectangle(4.0, 6.0, 0.0, 0.0)); shapes.push_back(std::make_uniqueTriangle( std::arraystd::pairdouble, double, 3{{{0,0}, {4,0}, {2,3}}} )); // 操作1绘制所有图形 DrawVisitor drawer; std::cout --- Drawing all shapes ---\n; for (const auto shape : shapes) { shape-accept(drawer); // 第一次分派确定shape的具体类型 // 在accept内部会调用 drawer.visitCircle/Rectangle/Triangle(*this) // 这是第二次分派确定操作DrawVisitor } // 操作2计算总面积 AreaVisitor areaCalculator; std::cout \n--- Calculating total area ---\n; for (const auto shape : shapes) { shape-accept(areaCalculator); } std::cout Total area: areaCalculator.getTotalArea() std::endl; // 新增一个操作变得极其简单只需实现一个新的访问者类 // 例如 JsonExportVisitor而无需修改任何Shape派生类。 return 0; }运行这段代码你会看到不同的访问者如何作用于同一组对象上执行完全不同的操作。这就是访问者模式的威力将操作与对象结构解耦。当需要新增一个“生成碰撞边界”的操作时你只需要创建一个新的CollisionBoundaryVisitor类实现三个visit方法。Shape类及其所有子类都无需改动它们对新的操作一无所知保持了高度的稳定性和封闭性。4. 进阶与优化应对现实项目的复杂性基础实现跑通了但在实际的大型C项目中直接使用上述经典结构可能会遇到一些棘手问题。我们需要利用现代C的特性来增强其健壮性、安全性和便利性。4.1 编译期依赖与“循环”问题经典实现要求访问者接口ShapeVisitor知道所有具体元素类Circle,Rectangle等。这带来一个维护上的痛点每增加一个新的图形类型如Ellipse你必须修改ShapeVisitor和ConstShapeVisitor这两个基类在所有派生访问者类中也可能需要添加新的visitEllipse方法除非你提供一个默认实现但这可能掩盖错误。这违反了“对修改关闭”的原则虽然修改点从元素类转移到了访问者基类但依然存在。一种常见的缓解方案是使用“默认处理”或“异常抛出”。在访问者基类中为每个visit方法提供一个默认实现比如调用一个通用的visitDefault或直接抛出异常。这样新增元素类型时只有那些真正需要处理该类型的访问者才需要重写对应方法其他访问者可以沿用默认行为。但这是一种“宽进严出”的策略可能会在运行时才发现遗漏了某个类型的处理。另一种更现代、更类型安全的方法是使用变体Variant和模式匹配C17的std::visit。这完全颠覆了经典访问者模式的结构将双分派从虚函数机制转移到std::variant的访问机制上。我们定义一个所有图形类型的std::variantusing ShapeVariant std::variantCircle, Rectangle, Triangle/*, ...*/;然后操作被实现为可调用对象函数、lambda、函数对象通过std::visit来应用// 操作1绘制 auto drawOperation [](auto shape) { // 这里shape的类型会被自动推导为 Circle, Rectangle 等 std::cout Drawing: ; if constexpr (std::is_same_vstd::decay_tdecltype(shape), Circle) { std::cout Circle with radius shape.getRadius(); } else if constexpr (std::is_same_vstd::decay_tdecltype(shape), Rectangle) { auto [w, h] shape.getDimensions(); std::cout Rectangle w x h; } // ... 其他类型 std::cout std::endl; }; // 操作2计算面积 auto areaOperation [](const auto shape) - double { if constexpr (std::is_same_vstd::decay_tdecltype(shape), Circle) { return M_PI * shape.getRadius() * shape.getRadius(); } else if constexpr (std::is_same_vstd::decay_tdecltype(shape), Rectangle) { auto [w, h] shape.getDimensions(); return w * h; } else { return 0.0; // 或者 static_assert 强制处理所有类型 } }; std::vectorShapeVariant shapes; shapes.emplace_back(Circle{5.0}); shapes.emplace_back(Rectangle{4.0, 6.0}); for (auto shapeVar : shapes) { std::visit(drawOperation, shapeVar); // 应用绘制操作 double area std::visit(areaOperation, shapeVar); // 应用面积计算操作 std::cout Area: area std::endl; }这种方式的优缺点非常明显优点编译期类型安全你可以使用if constexpr和模板确保所有类型都被处理或者让编译器在缺失时报错。无基类依赖不需要抽象的Shape基类和accept方法减少了虚函数开销和对象切片风险。添加新类型是破坏性的但添加新操作非常方便只需定义一个新的可调用对象。缺点对象存储std::variant有大小限制所有可能类型中最大的那个并且要求类型是可复制/可移动的。它存储的是值语义的对象可能涉及拷贝。开放性问题它实际上更适合于“操作开放类型封闭”的场景即类型集合已知且稳定操作频繁增加。这与经典访问者模式解决的问题类型稳定操作开放在侧重点上有所不同但常常可以相互替代。需要C17支持。在实际项目中你需要根据“类型变化更频繁”还是“操作变化更频繁”来权衡选择经典访问者模式还是std::variant方案。4.2 返回值处理的通用化在基础实现中AreaVisitor将结果存储在成员变量中。这种方式对于某些操作如遍历过程中收集信息是合适的但不够函数式且访问者对象有状态可能影响并发访问。我们可以利用C的模板和类型推导创造一个能返回任意类型结果的访问者接口。这需要将访问者接口模板化并使用decltype(auto)等现代特性。但这样会大幅增加接口的复杂度并且要求所有visit方法的返回类型必须兼容或者使用std::any、std::variant作为通用返回类型但这又引入了类型擦除的开销。一个更实用的折中方案是为不同的返回类型定义不同的访问者基类。例如定义一个返回void的VoidShapeVisitor和一个返回double的DoubleShapeVisitor。这虽然增加了一些重复代码但保持了接口的清晰和简单。在大多数业务场景下操作的类型是有限的这种方案完全可行。4.3 性能考量与“循环展开”优化访问者模式涉及两次虚函数调用accept-visitXxx这比一次虚函数调用或直接的非虚函数调用开销要大。在性能极其敏感的循环中例如遍历数百万个图形进行渲染这可能成为瓶颈。一种优化技巧是如果对象结构如std::vectorShape*是稳定的并且访问者需要频繁执行可以考虑在遍历前进行一次“类型分发”。例如将图形按类型分组存储std::vectorCircle*,std::vectorRectangle*然后在应用访问者时对每个同质容器进行循环这样内部循环就避免了虚函数分派编译器也有机会进行向量化优化。但这破坏了结构的统一性增加了内存管理的复杂度是一种典型的空间换时间、清晰度换性能的权衡只有在性能剖析Profiling明确指向此处为热点时才应考虑。另一个小优化是确保accept方法被定义为inline。由于它通常只是简单转发调用内联可以消除这次函数调用的开销。现代编译器在开启优化时通常能很好地做到这一点。5. 实战踩坑从设计到集成的完整心路历程理论再完美不经过实战的检验都是纸上谈兵。在我重构图形编辑器的过程中遇到了几个教科书上不会写的坑这里分享给你希望能帮你绕过去。5.1 坑一遗漏处理新增元素类型导致的运行时崩溃这是最经典的错误。我们增加了新的图形Ellipse实现了Ellipse类也更新了ShapeVisitor接口添加了virtual void visitEllipse(Ellipse) 0;。然后我们兴致勃勃地写了一个新的FancyRenderVisitor却只重写了visitCircle和visitRectangle忘记了重写visitEllipse。由于基类中的visitEllipse是纯虚函数程序在链接时就会报错这还算好的。更隐蔽的情况是如果基类提供了默认实现比如打印一个警告或抛出std::runtime_error程序能编译链接通过。但在运行时当一个Ellipse对象被遍历时就会调用到这个默认实现导致非预期的行为警告刷屏或直接崩溃抛出异常。这个问题在大型团队协作中尤其致命一个开发者添加了新类型另一个开发者添加了新访问者两者可能完全不知道对方的存在。我的解决方案是建立严格的代码审查和静态检查规则编译期检查尽量使用std::variant方案利用模板和if constexpr让编译器在缺失对某个类型的处理时报错。运行时断言在经典实现中访问者基类的默认visit方法实现里使用assert(false)或static_assert如果可能结合类型信息给出清晰的错误信息。void ShapeVisitor::visitEllipse(Ellipse ellipse) { // 不好的默认实现静默失败或模糊的异常 // throw std::runtime_error(Not implemented); // 较好的默认实现提供清晰信息 assert(false visitEllipse not implemented in this visitor. Visitor type: ...); // 或者使用typeid但注意RTTI开销 std::cerr ERROR: Unhandled type Ellipse in visitor of type typeid(*this).name() std::endl; std::terminate(); // 或根据错误处理策略决定 }单元测试覆盖为每个新增的访问者编写单元测试确保其能正确处理所有已知的元素类型。这可以通过模板元编程或简单的静态列表来实现。5.2 坑二访问者修改了元素状态导致的迭代器失效这是一个非常危险的场景。假设你有一个RemoveSmallShapesVisitor它的visitCircle逻辑是如果圆的半径小于阈值就从它所在的图形容器中删除这个圆。如果你在遍历std::vectorShape*的过程中直接执行删除操作会导致迭代器失效引发未定义行为通常是崩溃。// 危险代码 for (auto it shapes.begin(); it ! shapes.end(); it) { (*it)-accept(remover); // remover内部可能调用 shapes.erase(...) }正确的做法是将“标记”和“删除”分离。访问者只负责标记哪些元素需要被删除例如将一个指向元素的指针添加到一个std::vectorShape*待删除列表中遍历结束后再由容器管理者统一进行删除操作。或者使用更安全的遍历方法如从后向前遍历或者先收集待删除元素的索引/迭代器。5.3 坑三在accept方法中调用其他虚函数有时在具体元素的accept方法实现中你可能会 tempted 去调用该元素的其他虚方法。例如void Circle::accept(ShapeVisitor visitor) override { if (this-isVisible()) { // 调用虚函数 isVisible visitor.visitCircle(*this); } }这看起来没问题但它破坏了访问者模式的“纯洁性”。isVisible()本身可能也是一个需要在不同访问者中有不同行为的概念比如对“导出”访问者不可见的元素也要导出元数据对“渲染”访问者则不需要。更好的设计是将“可见性”判断逻辑也封装到一个访问者中或者作为元素的一个普通数据成员非虚函数。保持accept方法尽可能简单、直接是避免复杂递归和意外行为的关键。5.4 与现代C生态的集成智能指针与生命周期管理在我们的例子中使用了std::unique_ptrShape来管理元素的生命周期。当访问者需要存储对元素的引用例如CollectSelectedVisitor需要收集被选中的图形时要特别注意指针和引用的生命周期。存储引用如果访问者只是短暂使用在元素容器生命周期内存储Shape或Shape*是安全的。需要延长生命周期如果访问者需要将元素传递到其他上下文比如放入一个待处理队列则应该存储std::shared_ptrShape。这要求元素从一开始就用shared_ptr管理或者使用std::enable_shared_from_this。绝对不要存储裸指针或引用然后让原始容器销毁对象那会导致悬垂指针。一个实用的模式是在元素基类Shape中使用std::enable_shared_from_this并在accept方法中如果访问者需要共享所有权则传递shared_from_this()。class Shape : public std::enable_shared_from_thisShape { public: virtual ~Shape() default; virtual void accept(ShapeVisitor visitor) 0; // ... const版本 }; class Circle : public Shape { public: void accept(ShapeVisitor visitor) override { // 如果访问者接口设计为接受 shared_ptr visitor.visitCircle(shared_from_this()); // 注意对象必须由shared_ptr管理 } }; class CollectorVisitor : public ShapeVisitor { std::vectorstd::shared_ptrShape collected_; public: void visitCircle(std::shared_ptrCircle circle) override { collected_.push_back(std::move(circle)); } // ... };这增加了耦合但解决了生命周期管理的核心难题。是否采用取决于你的项目架构和对象所有权模型。访问者模式是一个强大的工具但它不是银弹。它的适用场景非常明确一个相对稳定的对象结构需要在此结构上定义多种多样且可能频繁变化的操作并且这些操作需要根据对象的具体类型进行不同的处理。在图形处理、编译器AST遍历、文档对象模型DOM操作、序列化/反序列化等场景中它都能大放异彩。然而如果对象结构本身就不稳定经常需要增加新的元素类型那么使用访问者模式将是灾难性的因为每加一个类型就要修改所有访问者类。此时std::variant加std::visit的方案或者甚至回归到简单的if-else类型判断可能是更务实的选择。最后关于代码风格我个人的体会是清晰的命名胜过复杂的注释。将访问者类命名为DrawVisitor、AreaCalculatorVisitor、JsonExporterVisitor远比Visitor1、Visitor2要好。同样accept和visit方法名是标准协议不要随意更改。保持接口的简洁和一致是降低团队协作中理解成本的关键。