Visitor模式要求每个子类必须实现所有visit纯虚函数,accept需在Element基类中定义并用*this调用以确保双分派,新增元素类型须同步修改所有Visitor实现,适合结构稳定、操作多的场景。

Visitor 类必须定义所有 visit 重载函数
访问者模式的核心约束是:每个 Visitor 子类必须为所有被访问的元素类型(比如 ElementA、ElementB)提供对应的 visit 函数。漏掉任何一个,编译就会报错——不是运行时错误,而是链接失败或未定义引用,比如 undefined reference to 'Visitor::visit(ElementB&)'。
常见做法是在基类 Visitor 中把所有 visit 声明为纯虚函数:
class Visitor {
public:
virtual void visit(ElementA& e) = 0;
virtual void visit(ElementB& e) = 0;
// 必须和 Element 类层次严格对齐
};
如果后续新增 ElementC,就必须同步修改所有已有的 Visitor 实现,否则编译不过。这是该模式最硬的扩展代价。
accept() 函数要写在 Element 基类里,且用 this 指针调用 visitor.visit(*this)
accept 是双分派的关键跳板,它必须是虚函数,并且实现固定为 v.visit(*this)。重点在于:*this 的静态类型必须是当前子类类型,才能触发正确的 visit 重载解析。
立即学习“C++免费学习笔记(深入)”;
错误写法(用基类指针传参)会导致始终调用 visit(Element&),失去多态性:
组合式C++代码评审方案,融合静态分析、AI推理、多轮迭代评审和C++专项检查,适用于PR审查、增量代码审查、全项目评审和代码质量评分,触发词包括review cpp、cpp代码评审、C++review、代码审查。
// ❌ 错误:this 转成了基类引用,丢失具体类型
void accept(Visitor& v) override { v.visit(*static_cast<Element*>(this)); }
正确写法(保持 this 的动态类型):
class Element {
public:
virtual void accept(Visitor& v) = 0;
};
class ElementA : public Element {
public:
void accept(Visitor& v) override { v.visit(*this); } // ✅ *this 是 ElementA&
};
class ElementB : public Element {
public:
void accept(Visitor& v) override { v.visit(*this); } // ✅ *this 是 ElementB&
};
Visitor 实现类通常不保存状态,但可以安全持有外部上下文指针
访问者对象本身一般设计为无状态(stateless),便于复用;但如果需要跨 visit 调用累积信息(比如统计元素个数、收集名称列表),可以在构造时传入一个可变上下文:
- 用
std::vector<std::string>*或int*持有外部变量地址 - 更推荐用
std::shared_ptr或引用捕获的 lambda 封装上下文,避免裸指针生命周期风险 - 不要在
Visitor成员变量里存std::unique_ptr去管理上下文——这会让访问者变成有状态且不可拷贝,破坏其轻量定位
例如,一个收集所有元素名称的访问者:
class NameCollector : public Visitor {
std::vector<std::string>& names_;
public:
NameCollector(std::vector<std::string>& n) : names_(n) {}
void visit(ElementA& e) override { names_.push_back("ElementA"); }
void visit(ElementB& e) override { names_.push_back("ElementB"); }
};
不支持运行时新增 Element 类型,也不适合频繁变更结构的场景
访问者模式本质是“把操作从数据结构中解耦”,但它把类型组合关系提前固化在编译期:每增加一个 Element 子类,就要改所有 Visitor 基类声明 + 所有已有实现;每增加一个 Visitor 子类,又得补全全部 visit 函数。
这意味着:
- 不适合插件式架构(比如用户动态加载新 Element 类型)
- 不适合配置驱动的节点系统(如图形编辑器中随时增删节点类型)
- 若 Element 层次稳定、操作种类多(导出、渲染、序列化、校验……),才真正体现价值
最容易被忽略的一点:很多人试图用 dynamic_cast 在 visitor 内部做类型判断来绕过重载,这等于放弃双分派,退化成条件分支,既慢又难维护——那就别用 Visitor 了。

















