Home > Net >  Extract data from templated derived class using virtual base class function
Extract data from templated derived class using virtual base class function

Time:08-15

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:

  1. What are the issues with this sort of approach aside from it seeming 'hacky' and not the most readable?

  2. Is there another, better approach to accomplish my end goal? I've looking into std::variant and std::any but std::variant doesn't seem as generic and std::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 States. 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).

  • Related