How to Implement the Visitor Pattern Without Inheritance

cvisitor-pattern

I write embedded firmware using C++. A common job for firmware is to "handle" different types of "messages" (e.g., in a communication protocol). The "C" way of doing this would be a really big switch statement like so:

void handleMessage(const AbstractMessage& msg)
{
    switch (msg.id())
    {
    case Message1::kId:
        //Do something with the specific message by casting
        //static_cast<const Message1&>(msg)
        break;
    case Message2::kId:
       //Do something else
       break;
    }
}

In OOP, a big switch statement like this is often a code smell as well as a maintenance headache. So, I typically leverage the features of C++ to implement this same process using the visitor pattern instead. This way, the compiler is essentially creating my "message lookup table" via virtual methods:

class Visitor
{
public:
    virtual void visit(const Message1& msg) {}
    virtual void visit(const Message2& msg) {}
};

class MyFantasticClass : public MessageVisitor
{
public:
    void handleMessage(const AbstractMessage& msg);
    void visit(const Message1& msg) override;
    void visit(const Message2& msg) override;
};

void MyFantasticClass ::handleMessage(const AbstractMessage& msg)
{
    msg.accept(*this);
}

This is great except for a couple problems.

First, because the "visitor" needs to inherit from an interface, I often find myself having to introduce multiple inheritance into my design. For example, MyFantasticClass my inherit to introduce a real "is-a" relationship while also needing to inherit from Visitor to implement a "is implemented in terms of" relationship. This multiple inheritance just adds more complexity and (potentially) overhead (e.g., due to additional "thunk" methods).

Second, I often find my classes visiting a message that originated internally. For example, the MyFantasticClass class creates a new message via a factory then wants to "visit" this specific message. Since the class inherits from the Visitor interfance, it now states to the outside world that it "is a" Visitor. But, the intent is not for other classes to use this interface.

My question is, is there another option I can look into?

One of my thoughts was to break the inheritance by introducing a private class which inherits from the visitor interface which is then a member of the main class

class MyFantasticClass : public MessageVisitor
{
public:
    void handleMessage(const AbstractMessage& msg);

private:
    class Impl : public MessageVisitor
    {
    public:
        void visit(const Message1& msg) override;
        void visit(const Message2& msg) override;
    };

    Impl _impl;
};

void MyFantasticClass ::handleMessage(const AbstractMessage& msg)
{
    msg.accept(_impl);
}

But this introduces the added complexity of me somehow needing to perhaps still have access to the members (variables and functions) of the MyFantasticClass class from within the visitor since it was this class that needed to "handle" the messages in the first place.

Best Answer

Based upon the comments on my question, one possible alternative would be to use std::variant and std::visit as such:

#include <variant>
#include <iostream>

class ClassA {};
class ClassB {};
class ClassC {};

using MyVariant = std::variant<ClassA, ClassB, ClassC>;

class MyVisitor
{
public:
    void visit(const MyVariant& v)
    {
        std::visit([this](auto&& arg){
            this->visit(arg);
        }, v);
    }

private:
    void visit(const ClassA& x)
    {
        std::cout << "MyVisitor visits ClassA" << std::endl;
    }
    
    void visit(const ClassB& x)
    {
        std::cout << "MyVisitor visits ClassB" << std::endl;
    }
    
    void visit(const ClassC& x)
    {
        std::cout << "MyVisitor visits ClassC" << std::endl;
    }
};

You can see a side-by-side example of using a traditional visitor versus std::variant here: https://godbolt.org/z/hKccz9qTe

Interestingly enough, the variant seems to produce smaller code (which is typically me desire for an embedded system) when compiled identically to the traditionalvisitor (using virtual methods). I confirmed this by compiling both using arm-none-eabi-g++ then running arm-none-eabi-size. This seems a bit odd since, from my understanding, std::visit essentially implements a vtable under the hood in order to do its work.

Related Topic