Reputation: 37
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
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);
}
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
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
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