访问者模式中为何需要accept()方法,不能直接调用visitor.visit()?
accept() method in the Visitor Pattern? Great question—this is one of the most common points of confusion when first learning the Visitor Pattern, so let’s unpack it clearly.
The Problem with Directly Calling visitor.visit(element)
Let’s say you have a hierarchy of Element derivatives: ConcreteElementA, ConcreteElementB, etc. Your ConcreteVisitor has overloaded visit methods for each of these specific element types, like:
class ConcreteVisitor : public Visitor { public: void visit(ConcreteElementA& elem) override { /* Do A-specific logic */ } void visit(ConcreteElementB& elem) override { /* Do B-specific logic */ } };
If you call theVisitor.visit(e) directly where e is an Element&, the compiler only knows e as an Element reference—not its actual runtime type. It will try to match the method signature visit(Element&) (which may not even exist!) instead of the correct overload for ConcreteElementA or ConcreteElementB. This is single dispatch: the method call is resolved based only on the type of the caller (theVisitor), not the type of the argument (e).
What accept() Does: Double Dispatch
The accept method fixes this by enabling double dispatch—two layers of method resolution that account for both the element’s runtime type and the visitor’s type:
- First, when you call
e.accept(theVisitor), sinceacceptis a virtual method onElement, the runtime dispatches to the correctacceptimplementation in the actual derived class (e.g.,ConcreteElementA::accept). - Inside that derived
acceptmethod, you havevisitor->visit(*this). Here,*thisis a reference to the derived type (ConcreteElementA&), so the compiler can resolve the correct overloadedvisitmethod on the visitor that matches this specific element type.
This is the magic: it lets you execute type-specific logic for each element without having to check the element’s type manually (like using dynamic_cast everywhere) or cluttering the element hierarchy with operation-specific code.
The Real Value of This Extra Layer
You’re right that adding accept to every element feels like extra boilerplate at first—but the tradeoff is huge for maintainability and scalability:
- Open/Closed Principle Compliance: You can add new operations (new
Visitorclasses) without modifying any of theElementderivatives. If you didn’t use the Visitor Pattern, adding a new operation would mean adding a new virtual method toElementand implementing it in every derived class—messy and error-prone when you have many elements. - Separation of Concerns: Operations (like printing, serializing, calculating values) are encapsulated in
Visitorclasses instead of being scattered across the element hierarchy. This keeps your element classes focused on their core responsibilities, not every possible operation someone might want to perform on them. - Avoids Brittle Type Checks: Without
accept, you’d have to write code like:
This breaks every time you add a new element type, forcing you to update all these conditional checks. Theif (auto* elemA = dynamic_cast<ConcreteElementA*>(&e)) { theVisitor.visit(*elemA); } else if (auto* elemB = dynamic_cast<ConcreteElementB*>(&e)) { theVisitor.visit(*elemB); }acceptmethod eliminates this entirely.
So while the accept method adds a small amount of upfront code, it pays off massively when you need to extend your codebase with new operations or manage a large hierarchy of elements.
内容的提问来源于stack exchange,提问作者Andrey Rubliov

