BehavioralPattern

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.

BehavioralPattern

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

Visitor

 

Rainer D 6 P2 500x500Modernes C++ Mentoring

Be part of my mentoring programs:

  • "Fundamentals for C++ Professionals" (open)
  • "Design Patterns and Architectural Patterns with C++" (open)
  • "C++20: Get the Details" (open)
  • "Concurrency with Modern C++" (starts March 2024)
  • Do you want to stay informed: Subscribe.

     

    • 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

    • Implements the accept operation

    I assume this graphic was too simple. The following picture from the Visitor Pattern on Wikipedia provides more insight.

    VisitorWiki

    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:

    visitor

    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.

    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

    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:

    singleDispatch

    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);
    }
    
    1. The member function accept of car iterates through all elements and calls elem->accept(visitor) on them: elem is a pointer and accept a virtual function => dynamic dispatch
    2. 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, Kris Kafka, 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, Matthieu Bolt, Stephen Kelley, Kyle Dean, Tusar Palauri, Dmitry Farberov, 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, moon, and Philipp Lenk.

    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

    Seminars

    I’m happy to give online seminars or face-to-face seminars worldwide. Please call me if you have any questions.

    Standard Seminars (English/German)

    Here is a compilation of my standard seminars. These seminars are only meant to give you a first orientation.

    • C++ – The Core Language
    • C++ – The Standard Library
    • C++ – Compact
    • C++11 and C++14
    • Concurrency with Modern C++
    • Design Pattern and Architectural Pattern with C++
    • Embedded Programming with Modern C++
    • Generic Programming (Templates) with C++
    • Clean Code with Modern C++
    • C++20

    Online Seminars (German)

    Contact Me

    Modernes C++ Mentoring,

     

     

    0 replies

    Leave a Reply

    Want to join the discussion?
    Feel free to contribute!

    Leave a Reply

    Your email address will not be published. Required fields are marked *