The Visitor Pattern
The Visitor Pattern encapsulates an operation executed on an object hierarchy as an object and enables it to define new operations without changing the object hierarchy.
The Visitor Pattern from the book “Design Patterns: Elements of Reusable Object-Oriented Software” is legendary for two reasons. First, for its complicatedness, and second, for a technique called double dispatch. Double dispatch describes the process of choosing the member function based on the object and the function arguments. Of course, the complicatedness of the Visitor Pattern is mainly due to the fact that double dispatch is not natively supported in C++, such as in Eiffel.
Let me first write about the Visitor Pattern before I discuss single dispatch and double dispatch.
Visitor Pattern
Purpose
- Encapsulates an operation executed on an object hierarchy in an object
- Enables to define new operations without changing the object hierarchy
Use Case
- Operations should be performed on an object hierarchy
- The operations change frequently
- The object hierarchy is stable
Structure
Visitor
- Defines the
visit
operation on the object structure
ConcreteVistor
- Implements the
visit
operation
Element
- Defines the
accept
operation that takes a visitor as an argument
ConcreteElement
Modernes C++ Mentoring
Do you want to stay informed: Subscribe.
- Implements the
accept
operation
I assume this graphic was too simple. The following picture from the Visitor Pattern on Wikipedia provides more insight.
By Fuhrmanator – Using the PlantUML software https://bit.ly/3MDbtCK, CC BY-SA 3.0, https://commons.wikimedia.org/w/index.php?curid=122709059
This picture gives a very good impression of the Visitor Pattern. Let me use it to explain the structure and the dynamic behavior of the Visitor Pattern.
- Structure
The Visitor Pattern has two type of hierarchies. The object hierarchy (CarElement
) and the operation hierarchy (CarElementVisitor
). The object hierarchy is pretty stable, but the operation hierarchy may support new operations. Both classes, CarElement
and CarElementVisitor
act as interfaces. This means that each concrete car element Wheel
, Engine
, Body
, and Car
must implement the accept(CarElementVisitor)
member function. Accordingly, each concrete operation CarElementDoVisitor
and CarElementPrintVisitor
must implement the four overloads visit(Wheel)
, visit(Engine)
, visit(Body)
, and visit(Car)
.
- Dynamic Behavior
Let’s assume that the operation CarElementPrintVisitor
is applied to the object hierarchy. The job of CarElementPrintVisitor
may be to print the name of the visited car part. First, a car element such as Engine
accepts the visitor (accept(CarElementVisitor)
) and uses the visitor to call back into the operations hierarchy (visitor.visit(this)
) using itself as an argument. This ensures that the visit(Engine)
overload on the CarElementPrintVisitor
is called. Visiting the Car
is special. A Car
consists of various car elements. Consequently, Car
‘s accept member function delegates the accept call to all its car parts.
Here is the crucial observation about the Visitor. It depends on two objects, which operation is performed: the visitor and the visited object.
Example
The following example literally translated the previous picture into code.
// visitor.cpp #include <iostream> #include <string> #include <vector> class CarElementVisitor; class CarElement { // (5) public: virtual void accept(CarElementVisitor& visitor) const = 0; virtual ~CarElement() = default; }; class Body; class Car; class Engine; class Wheel; class CarElementVisitor { // (6) public: virtual void visit(Body body) const = 0; virtual void visit(Car car) const = 0; virtual void visit(Engine engine) const = 0; virtual void visit(Wheel wheel) const = 0; virtual ~CarElementVisitor() = default; }; class Wheel: public CarElement { public: Wheel(const std::string& n): name(n) { } void accept(CarElementVisitor& visitor) const override { visitor.visit(*this); } std::string getName() const { return name; } private: std::string name; }; class Body: public CarElement { public: void accept(CarElementVisitor& visitor) const override { visitor.visit(*this); } }; class Engine: public CarElement { public: void accept(CarElementVisitor& visitor) const override { visitor.visit(*this); } }; class Car: public CarElement { public: Car(std::initializer_list<CarElement*> carElements ): elements{carElements} {} void accept(CarElementVisitor& visitor) const override { for (auto elem : elements) { elem->accept(visitor); } visitor.visit(*this); } private: std::vector<CarElement*> elements; // (7) }; class CarElementDoVisitor: public CarElementVisitor { void visit(Body body) const override { std::cout << "Moving my body" << '\n'; } void visit(Car car) const override { std::cout << "Starting my car" << '\n'; } void visit(Wheel wheel) const override { std::cout << "Kicking my " << wheel.getName() << " wheel" << '\n'; } void visit(Engine engine) const override { std::cout << "Starting my engine" << '\n'; } }; class CarElementPrintVisitor: public CarElementVisitor { void visit(Body body) const override { std::cout << "Visiting body" << '\n'; } void visit(Car car) const override { std::cout << "Visiting car" << '\n'; } void visit(Wheel wheel) const override { std::cout << "Visiting " << wheel.getName() << " wheel" << '\n'; } void visit(Engine engine) const override { std::cout << "Visiting engine" << '\n'; } }; int main() { std::cout << '\n'; Wheel wheelFrontLeft("front left"); Wheel wheelFrontRight("front right"); Wheel wheelBackLeft("back left"); Wheel wheelBackRight("back right"); Body body; Engine engine; Car car {&wheelFrontLeft, &wheelFrontRight, &wheelBackLeft, &wheelBackRight, &body, &engine}; CarElementPrintVisitor carElementPrintVisitor; engine.accept(carElementPrintVisitor); // (1) car.accept(carElementPrintVisitor); // (2) std::cout << '\n'; CarElementDoVisitor carElementDoVisitor; engine.accept(carElementDoVisitor); // (3) car.accept(carElementDoVisitor); // (4) std::cout << '\n'; }
At the beginning of the main
function, all parts of the car are created. Afterward, the engine and the car accept the carElementPrintVisitor
(lines 1 and 2). In lines (3) and (4), both objects are accepted by the carElementDoVisitor
. CarElement
(line 5) and CarElementVisitor
(line 5) are the abstract base classes of the object hierarchy and the operation hierarchy. According to the picture, the concrete car elements and visitors are created. The car is the most interesting car element because it holds its car elements in a std::vector<Element*>
(line 7).
Finally, here is the output of the program:
Related Patterns
The Visitor Pattern is probably the design pattern of the book “Design Patterns: Elements of Reusable Object-Oriented Software” which has the highest pattern density.
- The stable object hierarchy typically applies the Composite Pattern.
- The Iterator Pattern is often used to iterate through the object hierarchy.
- New elements can be created with a creational pattern, such as Factory Method or Prototype.
Pros and Cons
Pros
- A new operation (visitor) can be easily added to the operations hierarchy
- An operation is encapsulated in one visitor
- You can build a state while traversing the object hierarchy
Cons
- Modifying the object hierarchy with a new visited object
VisitedObject
is hard- You have to add or remove the visited object (
VisitedObject
) from the object hierarchy - You have to extend the interface of the visitor and add or remove the
visit(VisitObject)
member function to each concrete visitor
- You have to add or remove the visited object (
The complicatedness of the Visitor Pattern is mainly for one reason: double dispatch
Single Dispatch and Double Dispatch
Before I write about double dispatch, let me write about single dispatch, commonly known as virtual function calls.
Single Dispatch
In a single dispatch, the object decides which member function is called. To get virtuality in C++, you need two ingredients. An indirection such as a pointer or a reference and a virtual member function.
// singleDispatch.cpp #include <iostream> class Ball { public: virtual std::string getName() const = 0; virtual ~Ball() = default; }; class HandBall: public Ball { std::string getName() const override { return "HandBall"; } }; int main() { std::cout << '\n'; HandBall hBall; Ball* ballPointer = &hBall; // (1) std::cout << "ballPointer->getName(): " << ballPointer->getName() << '\n'; Ball& ballReference = hBall; // (2) std::cout << "ballReference.getName(): " << ballReference.getName() << '\n'; std::cout << '\n'; }
The expression Ball* ballPointer = &hBall
(line 1) has two types. The static type (Ball*
) and the dynamic type (Handball*
), return by the address of the operator &
. Because of the virtuality of the member function getName, the member function call is looked up at the run time. Consequentially, a dynamic dispatch happens, and the member function getName
is called. A similar argumentation holds for the reference used in line (2).
Here is the output of the program:
Now, let’s analyze the double dispatch used in the Visitor Pattern.
Double Dispatch
In double dispatch, it depends on two objects, which operation is performed.
This means, in the concrete case of the program visitor.cpp
, that the visitor and visited object manage together to call the appropriate member function.
To make it concrete: What happens in the call car.accept(carElementDoVisitor)
(line 4)? For simplicity, here is the member function accept
.
void accept(CarElementVisitor& visitor) const override { for (auto elem : elements) { elem->accept(visitor); } visitor.visit(*this); }
- The member function
accept
of car iterates through all elements and callselem->accept(visitor)
on them:elem
is a pointer andaccept
a virtual function => dynamic dispatch - Finally, the visitor calls
visit
on itself, using the visited element as an argument:visitor.visit(*this).
Consequentially, the appropriate overload of the visitor is called => static dispatch
Double dispatch, in the case of the visitor, is a ping/pong game between the element (car element) and the visitor. The car element applies a dynamic dispatch (override), and the visitor a static dispatch (overload).
What’s Next?
The Template Method is a behavioral design pattern that defines a template of an algorithm. In C++, we use a special variant of it: Non-Virtual Interface (NVI). I will present the Template Method in my next post.
Thanks a lot to my Patreon Supporters: Matt Braun, Roman Postanciuc, Tobias Zindl, G Prvulovic, Reinhold Dröge, Abernitzke, Frank Grimm, Sakib, Broeserl, António Pina, Sergey Agafyin, Андрей Бурмистров, Jake, GS, Lawton Shoemake, Jozo Leko, John Breland, Venkat Nandam, Jose Francisco, Douglas Tinkham, Kuchlong Kuchlong, Robert Blanch, Truels Wissneth, Mario Luoni, Friedrich Huber, lennonli, Pramod Tikare Muralidhara, Peter Ware, Daniel Hufschläger, Alessandro Pezzato, Bob Perry, Satish Vangipuram, Andi Ireland, Richard Ohnemus, Michael Dunsky, Leo Goodstadt, John Wiederhirn, Yacob Cohen-Arazi, Florian Tischler, Robin Furness, Michael Young, Holger Detering, Bernd Mühlhaus, Stephen Kelley, Kyle Dean, Tusar Palauri, Juan Dent, George Liao, Daniel Ceperley, Jon T Hess, Stephen Totten, Wolfgang Fütterer, Matthias Grün, Phillip Diekmann, Ben Atakora, Ann Shatoff, Rob North, Bhavith C Achar, Marco Parri Empoli, Philipp Lenk, Charles-Jianye Chen, Keith Jeffery, Matt Godbolt, and Honey Sukesan.
Thanks, in particular, to Jon Hess, Lakshman, Christian Wittenhorst, Sherhy Pyton, Dendi Suhubdy, Sudhakar Belagurusamy, Richard Sargeant, Rusty Fleming, John Nebel, Mipko, Alicja Kaminska, Slavko Radman, and David Poole.
My special thanks to Embarcadero | |
My special thanks to PVS-Studio | |
My special thanks to Tipi.build | |
My special thanks to Take Up Code | |
My special thanks to SHAVEDYAKS |
Modernes C++ GmbH
Modernes C++ Mentoring (English)
Rainer Grimm
Yalovastraße 20
72108 Rottenburg
Mail: schulung@ModernesCpp.de
Mentoring: www.ModernesCpp.org
Modernes C++ Mentoring,
Leave a Reply
Want to join the discussion?Feel free to contribute!