dr.umma
dr.umma

Reputation: 37

C++ - Identify derived class from base class pointer at runtime

I'm experimenting with state machines and the one that I'm trying to implement uses function pointers to represent states

typedef void (*State)(Signal const&)

class StateMachine
{
public:
    void exampleState(Signal const&);
private:
    State m_currentState;
}

Basically, I want to derive a separate class for each signal and in each state function the state machine must be able to determine which kind of signal has been received and execute the corresponding code. A solution that I came up with is something like

class Signal {};
class MySignal: public Signal {};

void StateMachine::exampleState(Signal const& signal){
    if (typeid(signal) == typeid(MySignal)){
        //code here
    }
    // other cases...
}

First of all I'm not sure that using typeid this way is good practice. Also, this only works if Signal has at least one virtual function.

Another solution would be to define a sort of type flag like an enum, and pass the corresponding one in the derived signal constructor

enum signalType{
  mySignalType
  //other types
}

class Signal {
public:
  Signal(signalType sig_type):m_type(sig_type){};
  const signalType m_type;
};

class MySignal: public Signal {
public:
  MySignal():Signal(mySignalType){};
};

void StateMachine::exampleState(Signal const& signal){
  switch (signal.m_type){
    case mySignalType:
      //code here
      break;
    // other cases...
  }
}

althoug this requires the enum to be extended each time a new signal class is written.

Is there a more elegant way of achieving this? Or maybe another technique that avoids this check at all? I remember having this problem in other scenarios as well, that's why the question in the title is more general than the example above.

Upvotes: 0

Views: 850

Answers (3)

Leonardo
Leonardo

Reputation: 1891

You should not have an if (typeid(signal) == typeid(MySignal)) statement in your class, that's a violation of the Liskov Substitution Principle. That means you're not hiding the details from the users of your Signal class, and that's a bad coding practice. Runtime polymorphism is about hiding the details and adhering to a "contract", so the StateMachine class can say "as long as signal plays according to the rules, I'm OK!", without really caring what the signal object is doing under the hood.

Sometimes, it is really hard to come up with a unified interface just because that's the nature of the problem you're solving. You best bet is to not try do abstract anything and use std::variant and std::visit to do the job for you:

#include <iostream>
#include <variant>

class SignalFoo {
  public:
    void doSomething() { std::cout << "    SignalFoo::doSomething!\n"; };
};

class SignalBar {
  public:
    void doSomething() { std::cout << "    SignalBar::doSomething!\n"; };
};

class SignalFooBar {
  public:
    void doSomething() { std::cout << "    SignalFooBar::doSomething!\n"; };
};

// Helper from the example in
// https://en.cppreference.com/w/cpp/utility/variant/visit
template <class... Ts>
struct overloaded : Ts... {
    using Ts::operator()...;
};
template <class... Ts>
overloaded(Ts...) -> overloaded<Ts...>;

// Syntax sugar
using SignalVariants = std::variant<SignalFoo, SignalBar, SignalFooBar>;

class StateMachine {
    public:
    void exampleState(SignalVariants& signal)
    {
        std::visit(overloaded{
                       [this](auto& r) {
                           std::cout << "General case!\n";
                           r.doSomething();
                       },
                       [this](SignalFooBar& r) {
                           std::cout << "SignalFooBar specific case!\n";
                           r.doSomething();
                       },
                   },
                   signal);
    }
};

int main(void) {

    SignalVariants foo = SignalFoo();
    SignalVariants bar = SignalBar();
    SignalVariants foobar = SignalFooBar();

    StateMachine fsm;

    fsm.exampleState(foo);
    fsm.exampleState(bar);
    fsm.exampleState(foobar);
}

See it in action

Conceptually, my answer is similar to @Remy's. The problem with that answer is that dynamic_cast<MySignal const *>(&signal) can fail during runtime and you get a nasty exception. With std::visit, the compiler will check wheter all valid variants have overloads, etc. If you take my example and comment one of the implementations:

class SignalFooBar {
  public:
    //void doSomething() { std::cout << "    SignalFooBar::doSomething!\n"; };
};

Compiling fails with:

error: 'class SignalFooBar' has no member named 'doSomething'
   41 |                            r.doSomething();

For a fantastic write on this, check C++ : Polymorphic inheritance without vtable

Upvotes: 0

Remy Lebeau
Remy Lebeau

Reputation: 596256

Use dynamic_cast instead of typeid:

class Signal {
public:
    virtual ~Signal() {}
};

class MySignal: public Signal {};

void StateMachine::exampleState(Signal const& signal){
    if (dynamic_cast<MySignal const *>(&signal)){
        //code here
    }
    // other cases...
}

Upvotes: 0

Syl
Syl

Reputation: 2783

What you want to achieve can be done through polymorphism.

Declare a method (or abstract method) in Signal, and implement it in MySignal:

class Signal {
public:
    virtual void my_method() const = 0;
};

class MySignal: public Signal {
public:
    void my_method() const override {
        // do something
    }
};

then call your method in exampleState, this will call the implemented method:

void StateMachine::exampleState(Signal const& signal){
    signal.my_method();
}

Upvotes: 4

Related Questions