I have a Device
object that can have 1 or more State
objects. I do not want to limit what sort of state the State
objects can describe so I've templated the value
of the State
objects. Since I want each Device
to keep a collection of these State
objects, I've derived them from a GenericState
class.
I'd like to be able to interact with pointers to this GenericState
class to read and write from the templated value, however templated virtual functions are not supported. My solution was to declare a purely virtual 'visiter' function in the base class that takes in a function with a void*
argument. The templated derived class implements the virtual function and calls the passed in function with the value of the State
. With this approach, I leave it up to the caller to choose what to do with the value (cast to a type, read, write, etc). Code snippets below.
My question is:
What are the issues with this sort of approach aside from it seeming 'hacky' and not the most readable?
Is there another, better approach to accomplish my end goal? I've looking into
std::variant
andstd::any
butstd::variant
doesn't seem as generic andstd::any
seems inappropriate. I've also considered static/dynamic casting but I'm not sure about their overhead.
My Device
class that 'owns' a collection of states:
class Device {
std::list<std::unique_ptr<GenericState>> states;
...
}
The GenericState
and State
classes themselves:
class GenericState {
public:
std::string name;
virtual ~GenericState() = default;
virtual void visitValue(std::function<void (void*)> func) = 0;
protected:
GenericState(std::string name): name(name) {}
};
template<typename T>
class State: public GenericState {
protected:
T value;
public:
State(const std::string& name, const T& value): GenericState(name), value(value) {}
const std::string getName() {return name; }
const T getValue() { return value; }
// Calls the provided function with a reference to the value for read/write
void visitValue(std::function<void (void*)> func) override {
func(&value);
}
};
An example of how a GenericState
pointer can be used to read/write the State
's value
:
State state = State(name, 5.0);
GenericState* genericState = &state;
// state.value = 5.0
double newVal = 0.2;
genericState->visitValue([&newVal](void* val){*(double *)val = newVal;});
// state.value = 0.2
double testVal = 0.0;
genericState->visitValue([&testVal](void* val){testVal = *(double *)val;});
// testVal = 0.2
Thanks.
CodePudding user response:
My - maybe opinionated - observation is that people overuse virtual
, maybe because of how C is usually taught. virtual
is very useful if you have to provide 30-years forward compatibility and module load without restart in a telco system; it's less useful when concrete types are known, esp. when you recompile the entire project after each change.
Here the caveat is, you'll lose all guarantees that type safety gives you. At each point where you call visitValue()
, you'll have to specify the type of the value, because that void*
hides it. The idea of type safety is, you should have type checks (thik of it as 'unit tests' in forms of type specifications) and these should fail if you miss it. Now, the key observation is, you will likely write genericState->visitValue();
way more often than the number of State
s. So you likely rather list the states once, e.g. in a variant
.
As soon as you have it in a variant
, you can simply visit
it and have the concrete type - from that point, you have type safety. All this you win for listing all the states once.
using GenericStateVar = std::variant<State, State2, State3>;
GenericStateVar genericState(State(name, 5.0));
double newVal = 0.2;
std::visit([&](auto&& state) { // you might limit the capture here
state.setValue(newVal); // consider making value public
}, genericState);
double testVal = 0.0;
std::visit([&](auto&& state) { // you might limit the capture here
testVal = state.getValue(); // consider making value public
}, genericState);
It's so simple with variant
, nothing else is needed. Also, variant
tends to be somewhat faster that virtual
, as the former is aware of all possible types (technically, it's a switch
vs. function pointer).