Visitor in C++

Behavioral Hard

In one sentence: Visitor represents an operation to be performed on the elements of an object structure, letting you define a new operation without changing the classes of the elements it operates on.

Real-world analogy

A building inspector arrives to survey a house and walks it room by room. The kitchen has no idea how to inspect itself — it knows nothing about gas regulations. All it does is open its door and announce what it is: "kitchen." That single fact is enough for the inspector to reach for the gas checks. The bathroom announces itself and gets the plumbing checks; the garage gets the wiring. The rooms hold the knowledge of what they are; the inspector holds the knowledge of what to do about it, and carries the clipboard and the running list of faults from room to room.

Next month a different specialist walks the identical route. The electrician does electrician things in each room; the valuer, in March, does valuer things. Three complete jobs performed over the house, and not one nail moved — the house never grew a beInspected(), beRewired(), or beValued() method.

The asymmetry shows up the moment you remodel. Convert the loft into a bedroom and every professional on your list has to be told what a loft bedroom is and what to do in one. New jobs are cheap; new kinds of room are expensive.

Mapping the analogy to the pattern:

  • The rooms — kitchen, bathroom, garage → the ConcreteElements: a hierarchy that's expected to sit still.
  • Opening the door and announcing "kitchen"Accept(visitor), followed by the visitor's kitchen-specific routine — the two hops of double dispatch, one to learn the room, one to pick the job.
  • The inspector → a ConcreteVisitor: one whole operation over the house, plus the clipboard where results accumulate as it goes.
  • The house and the order you walk it → the ObjectStructure, which enumerates the elements and hands each one to the visitor.
  • Next month's electrician → a new operation added without touching a single room; the converted loft → the expensive change, since every specialist must now learn it.

Another angle: a museum collection. A conservator, a photographer, and a valuer each pass over the same unchanged artefacts, each doing something different with a Roman coin than with an oil painting. New expertise arrives as a new person at the door, never as a change to the objects.

The problem

GoF's example is a compiler holding a program as an abstract syntax tree. The node classes barely change once the language is fixed, but the operations never stop arriving: type checking, code generation, pretty printing, optimization, metrics. Ours is a settled hierarchy of shapes — Circle, Rectangle, and a dozen more. Now the product team wants area calculation. Next sprint, SVG export. Then a JSON dump.

  1. Every operation touches every class. Each new feature means adding a method to all twelve shapes and recompiling everything.
  2. Unrelated concerns pile up. The Circle class ends up knowing about geometry, XML syntax, and JSON escaping.
  3. Sometimes you simply can't edit. The hierarchy may live in a library you don't own.

The solution

Move the operation out of the elements and into a visitor object:

  1. Write a visitor interface with one visit overload per concrete element type.
  2. Give every element one new method: accept(Visitor&), whose body is always v.visit(*this);.
  3. Each new operation is a new visitor class. The shapes never change again.

The trick that makes it work is double dispatch. A single virtual call picks a method based on one object's runtime type — C++ and most other languages give you only that. Here you need two types to decide: the visitor and the element. So the call happens in two hops — shape->accept(v) dispatches virtually on the shape, landing in (say) Circle::accept, where *this is statically known to be a Circle; that method then calls v.visit(*this), which dispatches virtually on the visitor and, thanks to overload resolution, picks the Circle overload. Two virtual calls, and both types are now known. GoF's summary is worth keeping: because accept binds the operation at runtime rather than statically, extending the element interface means writing one new Visitor subclass instead of many new Element subclasses.

Note: Somebody has to walk the structure, and GoF says you can put that job in any of three places. Usually the object structure does it — a collection just iterates and calls accept on each element, and a composite traverses itself by having each accept recurse into its children before calling the visit operation. Alternatively a separate Iterator drives it. Last, the visitor itself can traverse, which duplicates the traversal code in every concrete visitor and is only worth it when the traversal is genuinely irregular — when where you go next depends on the results computed so far. The example below takes the first option: main loops over the vector.

Structure

Structure of the Visitor pattern Structure of the Visitor pattern
Structure of the Visitor pattern

Two parallel hierarchies plus the thing being walked:

  • Visitor — declares one visit operation per concrete element class. The operation's signature identifies which class sent the request, which is how the visitor learns the element's concrete type and can then use that type's own interface. ShapeVisitor here.
  • ConcreteVisitor — implements every operation Visitor declares; together they make up one algorithm over the structure. It also provides the context for that algorithm and stores its local state, which typically accumulates results as the traversal proceeds. AreaCalculator and XmlExporter.
  • Element — declares the accept operation that takes a visitor. Shape here.
  • ConcreteElement — implements accept, always as the one-line call back into the matching visit operation. Circle and Rectangle.
  • ObjectStructure — can enumerate its elements and may offer a high-level interface for visiting them all. GoF notes it may be a Composite or just a plain collection; here it is the std::vector<std::unique_ptr<Shape>> in main.

C++ example

Two shapes, two visitors. The shape classes contain no export logic and no geometry — only accept.

#include <iomanip>
#include <iostream>
#include <memory>
#include <vector>

class Circle;
class Rectangle;

class ShapeVisitor {
public:
    virtual ~ShapeVisitor() = default;
    virtual void visit(const Circle& c) = 0;
    virtual void visit(const Rectangle& r) = 0;
};

class Shape {
public:
    virtual ~Shape() = default;
    virtual void accept(ShapeVisitor& v) const = 0;
};

class Circle : public Shape {
public:
    explicit Circle(double r) : radius(r) {}
    void accept(ShapeVisitor& v) const override { v.visit(*this); }
    double radius;
};

class Rectangle : public Shape {
public:
    Rectangle(double w, double h) : width(w), height(h) {}
    void accept(ShapeVisitor& v) const override { v.visit(*this); }
    double width, height;
};

class AreaCalculator : public ShapeVisitor {
public:
    void visit(const Circle& c) override { total += 3.14159 * c.radius * c.radius; }
    void visit(const Rectangle& r) override { total += r.width * r.height; }
    double total = 0.0;
};

class XmlExporter : public ShapeVisitor {
public:
    void visit(const Circle& c) override {
        std::cout << "<circle r=\"" << c.radius << "\"/>\n";
    }
    void visit(const Rectangle& r) override {
        std::cout << "<rect w=\"" << r.width << "\" h=\"" << r.height << "\"/>\n";
    }
};

int main() {
    std::vector<std::unique_ptr<Shape>> shapes;
    shapes.push_back(std::make_unique<Circle>(1.5));
    shapes.push_back(std::make_unique<Rectangle>(2.0, 3.0));

    XmlExporter xml;
    for (const auto& s : shapes) s->accept(xml);

    AreaCalculator area;
    for (const auto& s : shapes) s->accept(area);
    std::cout << std::fixed << std::setprecision(2)
              << "Total area: " << area.total << "\n";
}
Output
<circle r="1.5"/>
<rect w="2" h="3"/>
Total area: 13.07

The same loop over the same shapes produced XML once and a running total the next time — the only thing that changed was which visitor was passed in.

When should you use it?

GoF gives three conditions — and the third is really a warning. Use Visitor when:

  • The structure holds many classes of objects with differing interfaces, and you want to perform operations that depend on their concrete classes.
  • Many distinct and unrelated operations need performing on those objects and you want to avoid polluting the element classes with all of them. Visitor keeps each operation's parts together in one class. When the same structure is shared by several applications, it also lets you put an operation only in the application that needs it.
  • The element classes rarely change, but you often add operations. This is the load-bearing condition. Changing the object structure classes forces you to redefine the interface of every visitor, which is potentially expensive — so if the element classes change often, GoF's own advice is to skip Visitor and just put the operations in those classes.

Pros and cons

Pros (GoF's consequences)

  • Adding new operations is easy — a new operation over the structure is a new visitor class, where spreading the functionality across the elements would mean editing every one of them.
  • It gathers related operations and separates unrelated ones. Everything belonging to one algorithm lives in one visitor, and any data structures that algorithm needs stay hidden inside it, which simplifies both the elements and the algorithms.
  • A visitor can accumulate state as it moves through the structure. Without it, that state would be passed around as extra arguments to the traversal or, worse, kept in globals — AreaCalculator::total is exactly this.
  • It can visit across class hierarchies. An Iterator can only visit objects sharing a common parent class; a visitor can take types that are not related by inheritance at all — just add an overload for each.

Cons (also GoF's)

  • Adding a new ConcreteElement class is hard. Every new element adds an abstract operation to Visitor and an implementation to every concrete visitor. GoF is blunt about the resulting rule: decide whether you are more likely to change the algorithms over the structure or the classes in it, and use Visitor only in the first case.
  • It breaks encapsulation. The pattern assumes the element interface is rich enough for visitors to do their work, which in practice pushes you to expose an element's internal state publicly.
  • The accept/visit bounce is genuinely harder to follow than a plain method call.

Tip: If your elements are a small, closed set, std::variant plus std::visit gives you the same "operations outside the types" benefit with no accept boilerplate — and the compiler errors when you forget a case, instead of silently doing nothing.

How to remember it

Memory hook: "The tax inspector visits every room." The rooms stay dumb — they just open the door — and the inspector carries the operation, the paperwork, and the running total from room to room. Send a different professional next week and the house needs no renovation; add a new kind of room, though, and every professional has to be retrained. That asymmetry is the whole pattern: operations are cheap to add, element types are expensive.

  • Composite — GoF's primary relation: a visitor is how you apply an operation over an object structure defined by Composite, with each accept recursing into its children.
  • Iterator — one of the three places GoF says the traversal can live. It supplies the walk; the visitor supplies what to do at each stop. Note the limit an iterator has and a visitor doesn't: it can only traverse elements sharing a common parent type.