Search code examples
c++design-patternsvisitor-pattern

Visitor design pattern and multi layered class hierarchy


I have five classes with associated visitor:

struct Visitor
{
    virtual ~Visitor() = default;
    virtual void visit(A&) {}
    virtual void visit(B&) {}
    virtual void visit(C&) {}
    virtual void visit(D&) {}
    virtual void visit(E&) {}
};

struct A
{
    virtual ~A() = default;
    virtual void accept(Visitor& v) { v.visit(*this); }
};
struct B : A { void accept(Visitor& v) override { v.visit(*this); } };
struct C : A { void accept(Visitor& v) override { v.visit(*this); } };
struct D : C { void accept(Visitor& v) override { v.visit(*this); } };
struct E : C { void accept(Visitor& v) override { v.visit(*this); } };

All the instanced will be seen by user code at the maximum abstraction level possible, so they all will be seen as A&. User code needs to do two types of operations:

  1. print "I am C" if the instance is exactly of type C
  2. print "I am C" if the instance is of type C or any of its subtypes (namely D or E)

Operation 1 has a quite easy implementation, and almost works out of the visitor design pattern box with the infrastructure put in place:

struct OperationOne : Visitor
{
    void visit( C& ) override { std::cout << "I am C" << std::endl; }
};

And as expected the string "I am C" will be printed only once:

int main( )
{
    A a; B b; C c; D d; E e;
    std::vector<std::reference_wrapper<A>> vec = { a, b, c, d, e };

    OperationOne operation_one;

    for (A& element : vec)
    {
        element.accept(operation_one);
    }
}

Demo

The problem is: for the second operation, the whole infrastructure does not work anymore, assuming that we do not want to repeat the print code for D and E as well:

struct OperationTwo : Visitor
{
    void visit( C& ) override { std::cout << "I am C" << std::endl; }
    void visit( D& ) override { std::cout << "I am C" << std::endl; }
    void visit( E& ) override { std::cout << "I am C" << std::endl; }
};

As much as this would work, if the hierarchy changes and D is no longer a subtype of C, but for example a direct subtype of A, this code would still compile but would have not the expected behaviour at runtime, which is dangerous and undesirable.

One solution in order to implement operation 2 is to change the visitor infrastructure so that every visitable class would propagate the accepted visitor to its base classes:

struct B : A
{
    void accept(Visitor& v) override
    {
        A::accept( v );
        v.visit( *this );
    }
};

In this way, if the hierarchy changes we will have the compilation error, since the base class would not be found anymore by the compiler when trying to propagate the accepted visitor.

That said, we can now write the second operation visitor, and this time we do not need to duplicate the printing code for D and E:

struct OperationTwo : Visitor
{
    void visit(C&) override { std::cout << "I am C" << std::endl; }
}

As expected the string "I am C" will be printed three times in the user code when OperationTwo is used:

int main()
{
    A a; B b; C c; D d; E e;
    vector< reference_wrapper< A > > vec = { a, b, c, d, e };

    OperationTwo operation_two;

    for ( A& element : vec ) 
    {
        element.accept( operation_two );
    }
}

Demo

But wait: OperationOne and OperationTwo code is exactly the same! This means that by changing the infrastructure for the second operation, we basically broke the first one. In fact, now also OperationOne will print three times the string "I am C".

What can be done in order to have OperationOne and OperationTwo work seemlinglessly together? Do I need to combine the visitor design pattern with another design patter or do I need to not use visitor at all?


Solution

  • You might use as visitor the following which will dispatch with overload resolution:

    template <typename F>
    struct OverloadVisitor : Visitor
    {
        F f;
    
        void visit(A& a) override { f(a); }
        void visit(B& b) override { f(b); }
        void visit(C& c) override { f(c); }
        void visit(D& d) override { f(d); }
        void visit(E& e) override { f(e); }
    };
    

    and then

    struct IAmAC
    {
        void operator()( C& ) { std::cout << "I am C" << std::endl; }
        void operator()( A& ) {} // Fallback
    };
    
    using OperationTwo = OverloadVisitor<IAmAC>;
    

    Demo