Visitor
Learning Objectives
Add operations to a stable class hierarchy without modifying it.
Explain double dispatch.
Compare Visitor with std::variant/pattern matching.
Prerequisites: Modules 8, 35.
Intuition and Definition
Intent: represent an operation to be performed on the elements of an object structure; Visitor lets you define a new operation without changing the classes of the elements.
Double dispatch: the operation chosen depends on *two* dynamic types — the element's and the visitor's. element.accept(visitor) dispatches on the element; inside, visitor.visit(*this) dispatches on the visitor with the static type of *this selecting the overload.
Participants: Visitor (one visit overload per element type), concrete visitors (operations), Element (accept), concrete elements.
Why It Matters
When the element hierarchy is stable but operations keep growing (pricing, rendering, exporting, validating a document tree), Visitor groups each operation in one class instead of spreading it across all elements.
Syntax and Language Notes
# Python 3.10+: structural pattern matching is often simpler than Visitor
match shape:
case Circle(r=r): area = 3.14159 * r * r
case Rect(w=w, h=h): area = w * hComplete Example
#include <iostream>
#include <memory>
#include <string>
#include <variant>
#include <vector>
struct Engine; struct Battery; struct Tyre;
struct PartVisitor {
virtual ~PartVisitor() = default;
virtual void visit(const Engine&) = 0;
virtual void visit(const Battery&) = 0;
virtual void visit(const Tyre&) = 0;
};
struct Part {
virtual ~Part() = default;
virtual void accept(PartVisitor& v) const = 0; // first dispatch: on the element
};
struct Engine : Part { int kw; explicit Engine(int k) : kw(k) {} void accept(PartVisitor& v) const override { v.visit(*this); } };
struct Battery : Part { double kwh; explicit Battery(double k) : kwh(k) {} void accept(PartVisitor& v) const override { v.visit(*this); } };
struct Tyre : Part { int wearPercent; explicit Tyre(int w) : wearPercent(w) {} void accept(PartVisitor& v) const override { v.visit(*this); } };
struct ServiceCost : PartVisitor { // operation 1
double total = 0;
void visit(const Engine& e) override { total += 120 + e.kw * 0.5; }
void visit(const Battery& b) override { total += b.kwh > 50 ? 300 : 150; }
void visit(const Tyre& t) override { total += t.wearPercent >
…inspection:
engine 110 kW: oil change
battery 64 kWh: cell balance check
tyre 75% worn: REPLACE
tyre 40% worn: ok
service cost: 585
variant: total engine power 110 kWStep-by-Step Walkthrough
p->accept(report) dispatches virtually to Tyre::accept; inside, v.visit(*this) calls report.visit(const Tyre&) — the overload chosen by the static type Tyre, the target chosen by the visitor's dynamic type. That's double dispatch.
Each operation (ServiceCost, InspectionReport) lives in one class; the part classes never changed.
Cost: 120 + 55 (engine) + 300 (battery > 50 kWh) + 110 (worn tyre) + 0 = 585.
With std::variant, the set of types is closed and std::visit checks at compile time that every alternative is handled.
Common Mistakes
Visitor on an unstable hierarchy — every new element type changes *every* visitor.
Visitors that need private element data — forces public getters or friends.
Forgetting accept in a new element class (pure virtual helps).
Trade-offs and Alternatives
The expression problem: virtual functions make adding *types* easy and *operations* hard; Visitor makes adding *operations* easy and *types* hard. std::variant + std::visit, Java sealed interfaces + switch patterns, C# switch expressions and Python match provide the same "closed set, many operations" trade-off with less boilerplate.
Real-World Use
Compilers (AST visitors: type checking, code generation, optimization passes), document exporters, static analysers, CAD model processing, std::visit in modern C++ codebases.
Practice
Q1. (Easy) What makes Visitor "double" dispatch?
Answer: The operation depends on both the element's and the visitor's dynamic types (two virtual calls).
Q2. (Medium) What change is expensive with Visitor?
Answer: Adding a new element type — every visitor interface and implementation must change.
Q3. (Advanced) Write a PartCount visitor that counts tyres and other parts separately.
Answer: Implement PartVisitor with counters: visit(const Tyre&) increments tyres, the other overloads increment others; run accept over the structure. No part class changes, which is the point of the pattern.
Interview Question
Q1. "Explain the expression problem and how Visitor relates to it."
Answer: You have a set of types and a set of operations and want to add both without modifying existing code. OO class hierarchies make new types easy (new subclass) but new operations hard (touch every class). Visitor flips that: new operations are new visitor classes; new types require changing every visitor. Functional languages with algebraic data types and pattern matching behave like Visitor. Full solutions need language support (multimethods, type classes) or more complex designs; in practice you choose based on which axis changes more often.
Quick Quiz
Q1. Which method do elements implement?
Answer: accept(Visitor&).
Q2. Modern C++ alternative?
Answer: std::variant with std::visit.
Q3. Typical domain for Visitor?
Answer: Compiler ASTs (and other stable object structures with many operations).
Summary
Visitor adds operations to stable hierarchies via double dispatch.
It trades easy operations for hard new types; variants and pattern matching are lighter alternatives.
Related: Modules 8, 35, 50.