3

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.

7
  • 3
    You might want to look up std::variant and std::visit. Commented Aug 3, 2023 at 16:40
  • I'm wondering if private inheritance is one possible, simple solution as described here: softwareengineering.stackexchange.com/questions/208066/… Commented Aug 3, 2023 at 17:27
  • 4
    The Visitor pattern is to some extent a switch statement in disguise. Inheritance and std::visit really just help the compiler catch and enforce the cases you haven't implemented (e.g., if you're adding a new Visitor (a new operation), or if you add a new type of Element that you want to be able to visit (operate upon)). If you're simply switching on a type or on an identifier, then the "elements" are the different types/identifiers, and different "visitors" are just the different functions that contain switch statements. 1/2 Commented Aug 3, 2023 at 20:55
  • 3
    The primary design tradeoff is the same - it's fairly easy to add new functions/visitors, but it's hard to add new types of elements (cause you have to update all the functions/visitors to handle the new type in an appropriate way). There are other secondary tradeoffs like complexity vs type safety, etc. If you want the "changeability" tradeoff to be the other way around (easy to add new types, but harder to add new methods), then something like the Strategy pattern (+ perhaps a factory) is a better fit. 2/2 Commented Aug 3, 2023 at 20:55
  • This SO thread may help
    – radarbob
    Commented Aug 4, 2023 at 7:13

1 Answer 1

1

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.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.