batguano
batguano

Reputation: 141

How to implement generically typed member objects in C++?

I have an application which creates simple music visualization animations. These animations are driven by nodes, and each node has a bunch of parameters that could have one of several types: int, float, color, etc. The parameters can either have a user-set value, or can be connected to the output of another node.

I'm currently using a templated type, along with std::function<>, like this:

#include <functional>

template <class PT>
class Param
{
public:
    Param(PT value=PT()) : _value(value), _provider(nullptr) {}
    void    setValue(const PT &value) {_value = value;}
    void    setProvider(std::function<void(PT&)> provider) {_provider = provider;}

    void    getCurrentValue(PT &value)  {
        // update current member value
        if (_provider)
            _provider(_value);
        value = _value;
    }

private:
    PT                             _value;
    std::function<void(PT &value)> _provider;
};

I then instantiate parameters for an animated nodes like this:

class AnimationNode
{
public:
    AnimationNode(Model *model = nullptr);
    void evaluate();

private:
    Param<int>        _xoffset;
    Param<int>        _yoffset;
    Param<float>      _scale;
    Param<ColorType>  _color;
};

These parameters could be connected to a driver node, such as this one:

class SublevelMeter {    
public:
    SublevelMeter();
    void setRange(Subrange &_range);
    ...
    std::function<void(float&)> createProviderClosure();

private:
    float _level;
    ...
}

std::function<void(float&)> SublevelMeter::createProviderClosure() {
    return [this] (float &out) {out = _level;};
}

And connect one node to another by doing something like this:

AnimationNode::connectScaleToSublevel(SublevelMeter *slm) {
    _scale->setProvider(slm->createProviderClosure());
}

The problem is, I'd like there to be an abstract Param type that I can pass to objects, so rather than the code above, I could pass a param to my SublevelMeter:

SublevelMeter::connectToParam(Param *param) {
    param->setProvider(slm->createProviderClosure());
} 

This would also help when writing the routines that create my GUI editor widgets: the editor could figure out the correct type by introspection of the Param. But I'm not sure how to do this from a templated class, nor how the best way to implement the introspection in C++. (I'm coming at this from a python design background, which is perhaps encouraging me to think about this in a pythonic rather than C++ way; if there's a better way to approach this, I'd love to hear about it!)

I'm using Qt, so I've considered using QVariant, or other Qt Meta-Object stuff, but I'm not sure how to make that work, or if it would even be appropriate. (I'm not using Boost, and while I know it has certain type erasure facilities, I'm wary of wading into those waters...)

I'm interested in what the cleanest/"best" way to do this. Although efficiency is a consideration (getCurrentValue() is called many times per frame while the animation is playing) I can still probably afford run-time overhead of dynamic type stuff.

Upvotes: 3

Views: 132

Answers (2)

AngeloDM
AngeloDM

Reputation: 417

I implemented for you a simple class for the generic type management. This class is implemented without using template, so you can declare your variables and assign a value and a type directly at runtime. This implementation is very simple you should use it as reference to develop your own solution. In the following example I implemented the support for only 3 types: int, double and char* (C string). The main function shows you as to use the generic type class for both LVALUE and RVALUE assignment:

#include <stdio.h>
#include <stdlib.h>

enum Types {tInteger, tDouble, tString};

class TGenericType
{

private:
    char m_Value[100];
    Types m_Type;

protected:

public:

    void operator=(int AValue)
    {
        m_Type = tInteger;
        sprintf(m_Value, "%d", AValue);
    }

    operator int()
    {
        // try to convert the m_Value in integer
        return atoi(m_Value); // the result depend by atoi() function
    }

    void operator=(double AValue)
    {
        m_Type = tDouble;
        sprintf(m_Value, "%f", AValue);
    }

    operator double()
    {
        // try to convert the m_Value in double
        return atof(m_Value);  // the result depends by atof() function
    }

    void operator=(char* AValue)
    {
       m_Type = tString;
       strcpy(m_Value, AValue);
    }

    operator char*()
    {
        return m_Value;
    }


};

 int _tmain(int argc, _TCHAR* argv[])
{
    TGenericType LVar;

    // int assignment LVar used as LVALUE
    LVar = 10;

    // int assignment LVar used as RVALUE
    int i = LVar;

    // Double assignment LVar used as LValue
    LVar = 10.1;

    // double assignment LVar used as RVALUE
    double d = LVar;

    // costant string assignment LVar used as RVALUE
    LVar = "Ciao Mondo";

    // string copying LVar used as const string RVALUE
    char Buffer[100];
    strcpy(Buffer, LVar);

    return 0;
}

I tested above code on c++builder 32bit and c++builder (CLang) 64bit If my solution answer your question, please check it as answered.

Ciao from Italy! Angelo

Upvotes: 0

Anton Savin
Anton Savin

Reputation: 41341

At least the first part of your question is solvable without abstract Param:

class SublevelMeter {
    ...

    template<class PT>
    void connectToParam(Param<PT> *param) {
        param->setProvider(createProviderClosure<PT>());
    }

    // specialize this for different PTs
    template<class PT>
    std::function<void(PT&)> createProviderClosure();
}

If you really need to manipulate dynamic lists of Param-s, and you don't want to use any kind of RTTI, consider using Visitor pattern:

class Visitor;

class ParamBase
{
public:
    virtual ~ParamBase() = default;
    virtual void acceptVisitor(Visitor* v) = 0;
};

template <class PT>
class Param : public ParamBase
{
public:
    ...
    void acceptVisitor(Visitor* v) override;
};

class Visitor {
public:
    virtual ~Visitor() = default;

    void visit(ParamBase* p) {
        p->acceptVisitor(this);
    }

    virtual void visitParam(Param<float>* p) = 0;
    // add more functions for other Params
};  


class PrintVisitor : public Visitor {
public:
    void visitParam(Param<float>* p) override {
        std::cout << "visited Param<float>, value = " << p->getValue() << std::endl;
    }
};

template<class PT>
void Param<PT>::acceptVisitor(Visitor* v) {
    v->visitParam(this);
}

int main() {
    std::unique_ptr<ParamBase> p(new Param<float>(123.4f));
    std::unique_ptr<Visitor> v(new PrintVisitor());
    v->visit(p.get());
    return 0;
}

Upvotes: 0

Related Questions