The Visitor pattern and multiple dispatch

C++
Author

Quasar

Published

April 11, 2026

What is the Visitor pattern?

The Visitor pattern is a pattern that separates the algorithm from the object structure, which is the data for this algorithm. Using the Visitor pattern, we can add a new operation to the class hierarchy without modifying the classes themselves. The use of the Visitor pattern follows the open/closed principle of software design - a class (or another unit of code, such as a module) should be closed for modifications; once the class presents an interface to clients, the clients come to depend on this interface and the functionality it provides. This interface should remain stable; it should not be necessary to modify the classes in order to maintain the software and continue its development. At the same time, a class should be open for extensions - new functionality can be added to satisfy new requirements.

When viewed this way, the Visitor pattern allows us to add functionality to a class or an entire class hierarchy without having to modify the class. This feature can be particularly useful when dealing with public APIs - the users of the API can extend it with additional operations wihtout having the modify the source code.

A very different, more technical way to describe the Visitor pattern is to say that it implements double dispatch. This requires some explanation. Let’s start with the regular virtual function calls.

#include <print>

class Base{
    public:
    virtual void f() = 0;
};

class Derived1 : public Base{
    public:
    void f() override{ std::println("Derived1::f()"); }
};

class Derived2 : public Base{
    public:
    void f() override{ std::println("Derived2::f()"); }
};

int main(){
    Base* b{nullptr};
    b = new Derived1();
    b->f();
    b = new Derived2();
    b->f();
}

Compiler Explorer

If we invoke the b->f() virtual function through a pointer to the b base class, the call is dispatched to Derived1::f() or Derived2::f(), depending on the real type of the object. This is called single dispatch - the function that is actually called is determined by a single factor, the type of the object.

Now, let’s assume that the function f() also takes an argument that is a pointer to the base class:

#include <print>

class Base{
    public:
    virtual void f(Base* p) = 0;
    virtual void g() = 0;
};

class Derived1 : public Base{
    public:
    void f(Base* p) override{ std::println("Derived1::f()"); p->g(); }
    void g() override { std::println("Derived1::g()"); }
};

class Derived2 : public Base{
    public:
    void f(Base* p) override{ std::println("Derived2::f()"); p->g(); }
    void g() override { std::println("Derived2::g()"); }
};

int main(){
    Base* b{nullptr};
    Base* p{nullptr};

    b = new Derived1();
    p = new Derived1();
    b->f(p);
    p = new Derived2();
    b->f(p);

    b = new Derived2();
    p = new Derived1();
    b->f(p);
    p = new Derived2();
    b->f(p);
}

Compiler Explorer

The actual type of the p* object is also one of the derived classes. Now, the b->f(p) call can have four different versions: both the *b and *p objects can be of either of the two derived types. It is reasonable to want the implementation to do something different in each of these cases. This would be double dispatch - the code that ultimately runs is determined by two separate factors. Virtual functions do not provide a way to implement double dispatch directly, but the Visitor pattern does exactly that.

When presented in this way, it is not obvious that the double-dispatch visitor pattern has anything to do with the operation-adding visitor pattern. However, they are exactly the same pattern, and the two requirements are really one and the same. Here is a way to look at it that might help - if we want to add an operation to all classes in a hierarchy, that is equivalent to adding a virtual function, so we have one factor controlling the final disposition of each call, the object type. But, if we can effectively add virtual functions, we can add more than one - one for each operation we need to support. The type of operation is the second factor controlling the dispatch, similar to the argument to the function in our previous example. Thus, the operation-adding visitor is able to provide double dispatch. Alternatively, if we had a way to implement double dispatch, we could do what the Visitor pattern does - effectively add a virtual function for each operation we want to support.

Now, that we know what the Visitor pattern does, it is reasonable to ask, why would we want to do it? What is the use of double dispatch? And why would we want another way to add a virtual function substitute to a class when we can just add a genuine virtual function? Setting aside the case of the public API with unavailable source code, why would we want to add an operation externally instead of implementing it in every class?

Consider the example of the serialization/deserialization problem. Serialization is an operation that converts an object into a format that can be stored or transmitted (for example, written into a flat file). Deserialization is the inverse operation - it constructs a new object from its serialized and stored image. To support serialization and deserialization in a straight-forward object-oriented way, each class in the hierarchy would need two methods, one for each operation. But, what if there is more than one way to store an object? For example, we may need to write an object into a memory buffer, to be transmitted across the network and deserialized on another machine. Alternatively, we may need to save the object to disk, or else we may need to convert all objects in a container to a markup format such as JSON. The straightforward approach would have us add a serialization and a deserialization method to every object for every serialization mechanism. If a new and different serialization approach is needed, we have to go over the entire class hierarchy and add support for it.

An alternative is to implement the entire serialization/deserialization operation in a separate function that can handle all classes. The resulting code is a loop that iterates over all objects, with large decision tree inside of it. The code must interrogate every object and determine its type, for example, using dynamic casts. When a new class is added to the hierarchy, all serialization and deserialization implementations must be updated to handle the new objects.

Both implementations are difficult to maintain for large hierarchies. The Visitor pattern offers a solution - it allows us to implement a new operation - in our case, the serialization outside of the classes and without modifying them, but also without the downside of a huge decision tree in a loop. Note that the Visitor pattern is not the only solution to the serialization probleml C++ offers other possible approaches as well, but we focus on the Visitor pattern.

Basic Visitor in C++

The only way to really understand how the Visitor pattern operates is to work through an example. Let’s start with a very simple one. First we need a class hierarchy:

#include <string>

struct Pet{
    virtual ~Pet(){}
    Pet(std::string_view color) : m_color{ color } {}
    const std::string& color() const{ return m_color; }

    private:
    const std::string m_color;
};

struct Cat : public Pet{
    public:
    Cat(std::string_view color) : Pet{ color } {}
};

struct Dog : public Pet{
    public:
    Dog(std::string_view color) : Pet{ color } {}
};

int main(){}

Compiler Explorer

In this hierarchy, we have the Pet base class and several derived classes for different pet animals. Now, we want to add some operations to our classes, suhc as feed the pet or play with the pet. The implementation depends on the type of the pet, so these would have to be virtual functions if added directly to each class. This is not a problem for such a simple class hierarchy, but we are anticipating the future need for maintaining a much larger system in which modifying every class in the clas hierarchy is going to be expensive and time-consuming. We need a better way and we begin by creating a new class PetVisitor, which will be applied to every Pet object(visit it) and perform the operations we need. First, we need to declare the class.

struct Cat;
struct Dog;

struct PetVisitor{
    virtual void visit(Cat* c) = 0;
    virtual void visit(Dog* d) = 0;
};

We had to forward declare the Pet hierarchy classes because PetVistor has to be declared before the concrete Pet classes. Now, we need to make the Pet hierarchy visitable, which means we do need to modify it, but only once, regardless of how many operations we want to add later. We need to add a virtual function to accept the Visitor pattern to every class that can be visited:

struct Pet{
    virtual void accept(PetVisitor& v) = 0;
    const std::string& color(){ return m_color; }

    Pet(std::string_view color) : m_color{ color }{}
    private:
    std::string m_color;
};

struct Cat : public Pet{
    Cat(std::string_view color) : Pet{ color }{}
    void accept(PetVisitor& v) override { v.visit(this); }
};

struct Dog : public Pet{
    Dog(std::string_view color) : Pet{ color }{}
    void accept(PetVisitor& v) override { v.visit(this); }
};

Now, our Pet hierarchy is visitable and we have an abstract PetVisitor class. Everything is ready to implement the new operations for our clases(note that nothing that we have done so far depends on what operations we are going to add; we have created the visiting infrastructure that has to be implemented once). The operations are added by implementing concrete Visitor classes derived from PetVisitor:

#include <print>
struct FeedingVisitor : public PetVisitor{
    void visit(Cat* c) override{
        std::println("Feed tuna to the {} cat", c->color());
    }

    void visit(Dog* d) override{
        std::println("Feed steak to the {} dog", dog->color());
    }
};

struct PlayingVisitor : public PetVisitor{
    void visit(Cat* c) override{
        std::println("Play with a feather with the {} cat", c->color());
    }

    void visit(Dog* d) override{
        std::println("Play fetch with the {} dog", d->color());
    }
};

Assuming the visitation infrastructure is already build into our class-hierarchy, we can implement a new operation by implementing a derived Visitor class, and all its virtual functions overrides for visit(). To invoke one of the new operations on an object from our class hierarchy, we need to create a visitor and visit the object:

Cat c{ "organge tabby" };
FeedingVisitor fv;
c.accept(fv);   // Feeding tuna to the organge tabby cat

This example of a call is too simple in one important way - at the point of calling the visitor, we know the exact type of the object we are visiting. To make the example more realistic, we have to visit an object polymorphically.

std::unique_ptr<Pet> p(new Cat("orange tabby"));
FeedingVisitor fv;
p->accept(fv);

Here, we do not know at compile-time the actual type of the object pointed to by p; at the point where the visitor is accepted, p could come from different sources. Whule less common, the visitor can also be used polymorphically:

std::unique_ptr<Pet> p(new Cat("orange tabby"));
std::unique_ptr<PetVisitor> v(new FeedingVisitor());
p->accept(*v);

Compiler Explorer

When written this way, the code highlights the double dispatch aspect of the visitor pattern - the call to accept() ends up dispatched to a particular visit() function based on two factors - the type of the visitable *p object and the type of the visitor *v object.

We now have the most bare-bones example of the class object-oriented visitor in C++. Despite its simplicity, it has all the necessary components: an implementation for a large real-life class hierarchgy and multiple visitor operations has a lot more code, but no new kinds of code, just more of things we have already done. This example shows both aspects of the Visitor pattern: on the one hand, if we focus on the functionality of the software, with the visitation infrastructure now in place, we can add new operations without any changes to the classes themselves. On the other hand, if we look just at the way the operation is invoked, the accept() call, we have implemented double dispatch.

We can immediately see the appeal of the Visitor pattern, we can add any number of new operations without having to modify every class in the hierarchy. If a new class is added to the Pet hierarchy, it is impossible to forget to handle it - if we do nothing at all to the visitor, the accept() call on the new class will not compile since there is no corresponding visit() function to call. Once we add the new visit() overload to the PetVisitor base class, we have to add it to all derived classes as well; otherwise the compiler will let us know that we have a pure virtual function without an override. The latter is also one of the main disadvantages of the Visitor pattern - if a new class is added to the hierarchy. all visitors must be updated, whether the new classes actually need to support these operations or not. For this reason, it is sometimes recommended to use the visitor only on relatively stable hierarchies that do not have new classes added often. There is also an alternative visitor implementation that somewhat mitigates this problem.

Visitor generalizations and limitations

Our very first visitor in the previous section allowed us to effectively add a virtual function to every class in the hierarchy. That virtual function had no parameters and no return value. The former is easy to extend, there is no reason at all why our visit() functions cannot have parameters. Let’s expand our class hierarchy by allowing our pets to be kittens and puppies. This extension cannot be done using only the Visitor pattern - we need to add not only new operations but also new data memebers. The Visitor pattern can be used for the former, but the latter requires code changes.

struct Pet{
    void add_child(Pet* p){ m_children.push_back(p); }
    virtual void accept(PetVisitor&v, Pet* p = nullptr) = 0;
    Pet(std::string_view color) : m_color{ color }{}
    const std::string& color(){ return m_color; }

    private:
    std::vector<Pet*> m_children;
    std::string       m_color;
};

Each parent Pet object tracks its child objects (note that the container is a vector of pointers, not a vector of unique pointers, so the object doesn’t own its children, but merely has access to them). We have also added the new add_child() member function to to add objects to the vector. We could have done this with a visitor, but this function is non-virtual, so we have to add it only once to the base class, not to every derived class - the visitor is unnecessary here. The accept() function has been modified to have an additional parameter that would have to be added to all derived classes as well, where it is simply forwarded to the visit() function:

struct Cat{
    
    Cat(std::string_view color) : Pet(color) {}

    void accept(PetVisitor& v, Pet* p = nullptr) override{
        v.visit(this, p);
    }
};

struct Dog : public Pet{
    public:
    Dog(std::string_view color) : Pet(color) {}
    void accept(PetVisitor& v, Pet* p = nullptr){
        v.visit(this, p);
    }
}

The visit() function also has to be modified to accept the additional argument, even for the visitors that do not need it. Changing the parameters of the accept() function is, therefore, an expensive global operation that should not be done often, if at all. Note that, all overrides for the same virtual function in the hierarchy already have to have the same parameters. The Visitor pattern extends this restriction to all operations added using the same base Visitor object. A common workaround for this is to pass parameters using aggregates (classes or structures that combine multiple parameters together). The visit() function is declared to accept a pointer to the base aggregate class, while each visitor receives a pointer to a derived class that may have additional fields, and uses them as needed.

Now, our additional argument is forwarded through the chain of virtual function calls to the visitor, where we can make use of it. Let’s create a visitor that records the pet births and adds new pet objects, as children to their parent objects:

#include <vector>
#include <print>
#include <memory>
#include <cassert>

struct Pet;
struct Cat;
struct Dog;

struct PetVisitor{
   virtual void visit(Cat* c, Pet* p = nullptr) = 0;
   virtual void visit(Dog* d, Pet* p = nullptr) = 0;
};

struct Pet{
   void add_child(Pet* p){ m_children.push_back(p); }
   virtual void accept(PetVisitor&v, Pet* p = nullptr) = 0;
   Pet(std::string_view color) : m_color{ color }{}
   const std::string& color(){ return m_color; }

   private:
   std::vector<Pet*> m_children;
   std::string       m_color;
};

struct Cat : public Pet{
   Cat(std::string_view color) : Pet{ color }{}
   void accept(PetVisitor& v, Pet* p = nullptr) override { v.visit(this, p); }
};

struct Dog : public Pet{
   Dog(std::string_view color) : Pet{ color }{}
   void accept(PetVisitor& v, Pet* p = nullptr) override { v.visit(this, p); }
};

struct FeedingVisitor : public PetVisitor{
   void visit(Cat* c, Pet* p = nullptr) override{
       std::println("Feed tuna to the {} cat", c->color());
   }

   void visit(Dog* d, Pet* p = nullptr) override{
       std::println("Feed steak to the {} dog", d->color());
   }
};

struct PlayingVisitor : public PetVisitor{
   void visit(Cat* c, Pet* p = nullptr) override{
       std::println("Play with a feather with the {} cat", c->color());
   }

   void visit(Dog* d, Pet* p = nullptr) override{
       std::println("Play fetch with the {} dog", d->color());
   }
};

struct BirthVisitor : public PetVisitor{
   void visit(Cat* c, Pet* child) override{
       assert(dynamic_cast<Cat*>(child));
       c->add_child(child);
       std::println("{} gave birth to child {}", c->color(), child->color());
   }

   void visit(Dog* d, Pet* child) override{
       assert(dynamic_cast<Dog*>(child));
       d->add_child(child);
       std::println("{} gave birth to child {}", d->color(), child->color());
   }
};

int main(){
   std::unique_ptr<Pet> parent(new Cat("orange tabby"));
   Pet* child(new Cat("calico"));

   std::unique_ptr<PetVisitor> pv(new PlayingVisitor());
   std::unique_ptr<PetVisitor> fv(new FeedingVisitor());
   BirthVisitor bv;

   parent->accept(*pv);   // Play with a feather with orange tabby cat
   parent->accept(*fv);   // Feeding tuna to the organge tabby cat
   parent->accept(bv, child);
}

Compiler Explorer

Once we have established the parenthood relationships, we want to examine our pet families. That is another operation we want to add, which calls for another visitor.

#include <print>

struct FamilyTreeVisitor : public PetVisitor{
   void visit(Cat* c, Pet* p) override{
       std::println("Kittens : ");
       for(auto k : c->m_children){
           std::println("{}", k->color());
       }
   }

   void visit(Dog* d, Pet* p) override{
       std::println("Puppies : ");
       for(auto p : d->m_children){
           std::println("{}", p->color());
       }
   }
};

We have hit a slight problem, though, because as written, the code will not compile. The reason is that the FamilyTreeVisitor is trying to access the Pet::m_children data member, which is private. THis is another weakness of the Visitor pattern - from our point of view, the visitors add new operations to the classes, just like virtual functions, but from the compiler’s point of view, they are completely separate classes, not at all like member functions of the Pet classes and have no special access. Application of the visitor pattern usually requires that the encapsulation is relaxed in one of two ways - we can either allow public access to the data(directly or through get member functions) or declare the Visitor classes to be friends. In our example, we will follow the second route:

#include <vector>
#include <print>
#include <memory>
#include <cassert>

struct Pet;
struct Cat;
struct Dog;

struct PetVisitor{
    virtual void visit(Cat* c, Pet* p = nullptr) = 0;
    virtual void visit(Dog* d, Pet* p = nullptr) = 0;
};

struct Pet{
    friend struct FamilyTreeVisitor;

    void add_child(Pet* p){ m_children.push_back(p); }
    virtual void accept(PetVisitor&v, Pet* p = nullptr) = 0;
    Pet(std::string_view color) : m_color{ color }{}
    const std::string& color(){ return m_color; }

    private:
    std::vector<Pet*> m_children;
    std::string       m_color;
};

struct Cat : public Pet{
    Cat(std::string_view color) : Pet{ color }{}
    void accept(PetVisitor& v, Pet* p = nullptr) override { v.visit(this, p); }
};

struct Dog : public Pet{
    Dog(std::string_view color) : Pet{ color }{}
    void accept(PetVisitor& v, Pet* p = nullptr) override { v.visit(this, p); }
};

struct FeedingVisitor : public PetVisitor{
    void visit(Cat* c, Pet* p = nullptr) override{
        std::println("Feed tuna to the {} cat", c->color());
    }

    void visit(Dog* d, Pet* p = nullptr) override{
        std::println("Feed steak to the {} dog", d->color());
    }
};

struct PlayingVisitor : public PetVisitor{
    void visit(Cat* c, Pet* p = nullptr) override{
        std::println("Play with a feather with the {} cat", c->color());
    }

    void visit(Dog* d, Pet* p = nullptr) override{
        std::println("Play fetch with the {} dog", d->color());
    }
};

 struct BirthVisitor : public PetVisitor{
    void visit(Cat* c, Pet* child) override{
        assert(dynamic_cast<Cat*>(child));
        c->add_child(child);
        std::println("{} gave birth to child {}", c->color(), child->color());
    }

    void visit(Dog* d, Pet* child) override{
        assert(dynamic_cast<Dog*>(child));
        d->add_child(child);
        std::println("{} gave birth to child {}", d->color(), child->color());
    }
 };


 struct FamilyTreeVisitor : public PetVisitor{
    void visit(Cat* c, Pet* p) override{
        std::println("Kittens");
        for(auto k : c->m_children){
            std::println("\t{}", k->color());
        }
    }

    void visit(Dog* d, Pet* p) override{
        std::println("Puppies");
        for(auto p : d->m_children){
            std::println("\t{}", p->color());
        }
    }
 };

int main(){
    std::unique_ptr<Pet> parent(new Cat("orange tabby"));
    Pet* child1(new Cat("calico"));
    Pet* child2(new Cat("garfield"));

    std::unique_ptr<PetVisitor> pv(new PlayingVisitor());
    std::unique_ptr<PetVisitor> fv(new FeedingVisitor());
    BirthVisitor bv;

    parent->accept(*pv);   // Play with a feather with orange tabby cat
    parent->accept(*fv);   // Feeding tuna to the organge tabby cat
    parent->accept(bv, child1);
    parent->accept(bv, child2);

    FamilyTreeVisitor tv;
    parent->accept(tv);
}

Compiler Explorer

Unlike BirthVisitor, FamilyTreeVisitor does not need the additional argument.

Now, we have visitors that implement operations with parameters. What about return values? Technically, there is no requirement for visit() and accept() functions to return void. They can return anything else. However, the limitation that they have to all return the same type usuallyt makes this capability useless. Virtual functions can have covariant return types, where the base class virtual function returns an object of some class and the derived class overrides return objects derived from that class, but even that is usually too limiting. There is another, much simpler solution - the visit() functions of every visitor object has full access to the data members of the object. THere is no reason why we cannot store the the return value in the Visitor class itself and access it later. This fits well with the most common use where each visitor adds a different operation and is likely to have a unique return type, but the operation itself usually has the same return type for all the classes in the hierarchy. For example, we can make our FamilyTreeVisitor count the total number of children and return the value through the visitor object.

#include <vector>
#include <print>
#include <memory>
#include <cassert>

struct Pet;
struct Cat;
struct Dog;

struct PetVisitor{
    virtual void visit(Cat* c, Pet* p = nullptr) = 0;
    virtual void visit(Dog* d, Pet* p = nullptr) = 0;
};

struct Pet{
    friend struct FamilyTreeVisitor;

    void add_child(Pet* p){ m_children.push_back(p); }
    virtual void accept(PetVisitor&v, Pet* p = nullptr) = 0;
    Pet(std::string_view color) : m_color{ color }{}
    const std::string& color(){ return m_color; }

    private:
    std::vector<Pet*> m_children;
    std::string       m_color;
};

struct Cat : public Pet{
    Cat(std::string_view color) : Pet{ color }{}
    void accept(PetVisitor& v, Pet* p = nullptr) override { v.visit(this, p); }
};

struct Dog : public Pet{
    Dog(std::string_view color) : Pet{ color }{}
    void accept(PetVisitor& v, Pet* p = nullptr) override { v.visit(this, p); }
};

struct FeedingVisitor : public PetVisitor{
    void visit(Cat* c, Pet* p = nullptr) override{
        std::println("Feed tuna to the {} cat", c->color());
    }

    void visit(Dog* d, Pet* p = nullptr) override{
        std::println("Feed steak to the {} dog", d->color());
    }
};

struct PlayingVisitor : public PetVisitor{
    void visit(Cat* c, Pet* p = nullptr) override{
        std::println("Play with a feather with the {} cat", c->color());
    }

    void visit(Dog* d, Pet* p = nullptr) override{
        std::println("Play fetch with the {} dog", d->color());
    }
};

 struct BirthVisitor : public PetVisitor{
    void visit(Cat* c, Pet* child) override{
        assert(dynamic_cast<Cat*>(child));
        c->add_child(child);
        std::println("{} gave birth to child {}", c->color(), child->color());
    }

    void visit(Dog* d, Pet* child) override{
        assert(dynamic_cast<Dog*>(child));
        d->add_child(child);
        std::println("{} gave birth to child {}", d->color(), child->color());
    }
 };


 struct FamilyTreeVisitor : public PetVisitor{
    FamilyTreeVisitor() : m_child_count{0uz} {}
    void reset(){ m_child_count = 0; }
    size_t get_child_count(){ return m_child_count; }

    void visit(Cat* c, Pet* p) override{
        visit_impl(c, "Kittens");
    }

    void visit(Dog* d, Pet* p) override{
        visit_impl(d, "Puppies");
    }

    private:
    size_t m_child_count;

    template<typename T>
    void visit_impl(T* t, const char* s){
        std::println("{}", s);
        for(auto p : t->m_children){
            std::println("\t{}", p->color());
            ++m_child_count;
        }
    }
 };

int main(){
    std::unique_ptr<Pet> parent(new Cat("orange tabby"));
    Pet* child1(new Cat("calico"));
    Pet* child2(new Cat("garfield"));

    std::unique_ptr<PetVisitor> pv(new PlayingVisitor());
    std::unique_ptr<PetVisitor> fv(new FeedingVisitor());
    BirthVisitor bv;

    parent->accept(*pv);   // Play with a feather with orange tabby cat
    parent->accept(*fv);   // Feeding tuna to the organge tabby cat
    parent->accept(bv, child1);
    parent->accept(bv, child2);

    FamilyTreeVisitor tv;
    parent->accept(tv);

    std::println("{} kittens total", tv.get_child_count());
}

Compiler Explorer

This approach imposes some limitations in multithreaded programs - the visitor is now not thread safe since multiple threads cannot use the same Visitor object to visit different pet objects. The most common solution is to use one visitor object per thread.

Let’s return for a moment and examine the FamilyTreeVisitor implementation again. Note that it iterates over the child objects of the parent object and calls the same operation on each one, in turn. It does not, however, process the children of the child object - our family tree is only one-generation deep. The problem of visiting objects that contain other objects is very general and occurs rather often. Our motivational example - the problem of serialization demonstrates this need perfectly - every complex object is serialized by serializing its components one by one and they in turn are serialized the same way, until we get all the way down to the built-in types such as int and double which we know to read and write.

Visiting complex objects

In the last section, we saw, how the Visitor pattern allows us to add new operations to the existing hierarchy. In one of the examples, we visited a complex object that contained pointers to other objects. The visitor iterated over these pointers, in a limited way. We are now going to consider the general problem of visiting objects that are composed of other objects, or objects that contain other objects and build up to the demonstration of a working serialization./deserialization solution at the end.

Visiting composite objects

The general idea of visiting complex objects is quite straightforward - when visiting the object itself, we generally do not know all the details of how to handle each component or contained object. But there is something else that does - the visitor for that object type is written specifically to handle that class and nothing else. This observation suggests that the correct way to handle the component objects is to simply visit each one, and thus delegate the problem to someone else.

Let’s first demonstrate this idea on the example of a simple container class, such as the Shelter class, which can contain any number of pet objects representing the pets waiting for adoption:

struct Shelter{
    void add(Pet* p){
        m_pets.emplace_back(p);
    }

    void accept(PetVisitor& v){
        for(auto& p : m_pets){
            p->accept(v);
        }
    }

    private:
    std::vector<std::unique_ptr<Pet>> m_pets;
};

This class is essentially an adapter to make a vector of pet objects visitable(more on the adapter pattern in another blog post). Note that the objects of this class do own the pet objects they contain - when the Shelter object is destroyed, so are all the Pet objects in the vector. Any container of unique pointers is a container that owns its contained objects; this is how polymorphic objects should be stored in a container such as std::vector.

The code relevant to our current problem is, of course, Shelter::accept(), which determines how a Shelter object is visited. As you can see, we do not invoke the visitor on the shelter object itself. Instead, we delegate the visitation to each of the containing objects. Since our Visitors are already written to handle Pet objects, nothing more needs to be done. When Shelter is visited by, say, FeedingVisitor, every pet in the shelter gets fed, and we didn’t have to write any special code to make it happen.

Visitation of composite objects is done in a similar manner - if an object is composed of several smaller objects, we have to visit each of these objects. Let’s consider an object representing a family with two family pets, a dog and a cat(the humans who serve the pets are not included in the following code, but we assume they are there too):

struct Family{
    public:
    Family(const car* cat_color, const char* dog_color)
    : m_cat{ cat_color }
    , m_dog{ dog_color }
    {}

    void accept(PetVisitor& v){
        m_cat.accept(v);
        m_dog.accept(v);
    }

    private:
    Cat m_cat;
    Dog m_dog;
};

Again, visiting the family with a visitor from the PetVisitor hierarchy is delegated so that each Pet object is visited, and the visitors already have everything they need to handle these objects. Now, at last, we have all the pieces we need to tackle the problem of serialization and deserialization of arbitrary objects.

Serialization and Deserialization with visitor

The problem itself was described in detail in the previous section - for serialization, each object needs to be converted to a sequence of bits, and these bits need to be stored, copied or sent. The first part of the action depends on the object(each object is converted differently), but the second part depends on the specific application of the serialization(saving to the disk is different from sending across the network). The implementation depends on two factors, hence the need for double dispatch, which is exactly what the Visitor pattern provides. Furthermore, if we have a way to serialize some object and then deserialize it (reconstruct the object from the sequence of bits), we should use the same method when this object is included in another object.

To demonstrate serialization/deserialization of a class hierarchy using the Visitor pattern. we meed a more complex hierarchy than the toy examples we have used so far. Let’s consider this hierarchy of two-dimensional geometric objects:

struct Geometry{
    virtual ~Geometry(){}
};

struct Point : public Geometry{
    Point() = default;
    Point(double x, double y) : x_{x}, y_{y}
    {}

    private:
    double x_;
    double y_;
};

struct Circle : public Geometry{
    Circle() = default;
    Circle(Point center, double radius) 
    : center_{ center }
    , radius_{ radius }
    {}

    private:
    Point center_;
    double radius_;
};

struct Line : public Geometry{
    Line() = default;
    Line(Point p1, Point p2) 
    : p1_{ p1 }
    , p2_{ p2 }
    {}

    private:
    Point p1_;
    Point p2_;
}

All objects derived from the abstract Geometry base class, but the more complex object contains one or more of the simpler objects; for example, Line is defined by two Point objects. Note that, at the end of the day, all our objects are made of double numbers, and therefore, will serialize into a sequence of numbers. The key is knowing, which double represents which field of which object: we ned this to restore the original objects correctly.

To serialize these objects using the Visitor pattern, we follow the same process we used in the last section. First, we need to declare the base Visitor class:

struct Visitor{
    virtual void visit(double& x) = 0;
    virtual void visit(Point& p) = 0;
    virtual void visit(Circle& c) = 0;
    virtual void visit(Line& l) = 0;
};

There is one additional detail here - we can also visit double values; each visitor would need to handle them appropriately(write them, read them and so on). Visiting any geometry object will result, eventually, in visiting the numbers it is composed of.

Our base Geometry class and all classes derived from it need to accept this visitor:

struct Geometry{
    virtual ~Geometry(){}
    virtual void accept(Visitor& v) = 0;
};

There is of course, no way to add an accept() member function to double, but we won’t have to. The accept() member functions of the derived classes, each of which is composed of one or more members and other classes, visit every data member in order:

void Point::accpet(Visitor& v){
    v.visit(x_);
    v.visit(y_);
}

void Circle::accept(Visitor& v){
    v.visit(center_);
    v.visit(radius_);
}

void Line::accept(Visitor& v){
    v.visit(p1_);
    v.visit(p2_);
}

The concrete Visitor classes, all derived from the base Visitor class, are responsible for the specific mechanisms of serialization and deserialization. The order in which the objects are broken down into their parts, all the way down to the numbers, is controlled by each object, but the visitors determine what is done with these numbers. For example, we can serialize all objects into a string using formatted I/O(similar to what we get if we print the numbers into cout).

struct StringSerializeVisitor : public Visitor{
    void visit(double& x) override{
        sstream_ << x << " ";
    }
    void visit(Point& p) override{
        p.accept(*this);
    }

    void visit(Line& l) override{
        l.accept(*this);
    }

    std::string str(){
        return sstream_.str();
    }

    private:
    std::stringstream sstream_;
};

The string is accumulated in stringstream until all the ncessary objects are serialized.

Point p1{1.0, -1.0};
Point p2{3.0, 4.0};
Line l{p1, p2};
Circle c{p1, 5.0};

StringSerializeVisitor serializer;
serializer.visit(l);
serializer.visit(c);
std::string s{serializer.str()};

Now, that we have the objects printed into the s string, we can also restore them from this string perhaps on a different machine(if we arranged for the string to be sent there). First, we need the deserializing visitor:

struct StringDeserializeVisitor : public Visitor{
    StringDeserializeVisitor(const std::string& s){
        sstream_.str(s);
    }

    
};