views:

94

answers:

5

What I would like to do (in C++) is create a 'Parameter' data type which has a value, min, and max. I would then like to create a container for these types.

E.g. I have the following code:

    template <typename T>
    class ParamT {
    public:
        ParamT() {
        }

        ParamT(T _value):value(_value) {
        }

        ParamT(T _value, T _vmin, T _vmax):value(_value), vmin(_vmin), vmax(_vmax) {
        }


        void setup(T vmin, T vmax) {
            this->vmin      = vmin;
            this->vmax      = vmax;
        }

        void setup(T value, T vmin, T vmax) {
            setup(vmin, vmax);
            setValue(value);
        }

        T operator=(const T & value) {
            setValue(value);
        }



        void setValue(T v) {
            value = v;
        }

        T getValue() {
            return value;
        }

        operator T() {
            return getValue();
        }

    protected:

        T       value;
        T       vmin;
        T       vmax;
    };

    typedef ParamT<int>     Int;
    typedef ParamT<float>   Float;
    typedef ParamT<bool>    Bool;

In an ideal world my Api would be something like:

std::map<string, Param> params;
params["speed"] = PFloat(3.0f, 2.1f, 5.0f);
params["id"] = PInt(0, 1, 5);

or

params["speed"].setup(3.0f, 2.1f, 5.0f);
params["id"].setup(0, 1, 5);

and writing to them:

params["speed"] = 4.2f;
params["id"] = 1;

or

params["speed"].setValue(4.2f);
params["id].setValue(1);

and reading:

float speed = params["speed"];
int id = params["id"];

or

float speed = params["speed"].getValue();
int id = params["id"].getValue();

Of course in the code above, ParamT has no base class so I cannot create a map. But even if I create a base class for it which ParamT extends, I obviously cannot have different getValues() which return different types. I thought about many solutions, including setValueI(int i), setValuef(float f), int getValueI(), float getValueF(), or a map for ints, a map for floats etc. But all seem very unclean. Is it possible in C++ to implement the above API?

At the moment I am only concerned with simple types like int, float, bool etc. But I would like to extend this to vectors (my own) and potentially more.

A: 

Well, it's easy to make a container store just about anything. As you said, you could make a common base class and have the map just store a pointer to that. The hard part is knowing what data type they are when you're retrieving them and using it. I have something like this in my main project where I'm mixing compile-time type determined c++ code and run-time type determined code from another language. So I embed into the class it's datatype code so that I can do a switch() statement on it. You could have something like this:

    enum DataTypeCode
    {
        UNKNOWN,
        INT,
        FLOAT
    };

    template <class DataType>
    DataTypeCode GetDataTypeCode()
    {
        return UNKNOWN;
    }

    template <>
    DataTypeCode GetDataTypeCode<int>()
    {
        return INT;
    }

    template <>
    DataTypeCode GetDataTypeCodE<float>(
    {
        return FLOAT;
    }

    class BaseParam
    {
    public:
        virtual ~BaseParam() {}
        virtual DataTypeCode GetDataTypeCode()=0;
    };

    template <class DataType>
    class Param : public BaseParam
    {
    public:
        DataTypeCode GetDataTypeCode()
        {
            return ::GetDataTypeCode<DataType>();
        }
    }

and you have to store it as a pointer to take care of polymorphism:

    std::map<string,BaseParam*> Params

    Params["speed"]=new Param<float>(...)

    BaseParam* pMyParam=Params["speed"];
    switch (pMyParam->GetDataTypeCode())
    {
        case INT:
            //dosomething with int types
        case FLOAT:
            //dosomething with float types
    }

It's not pretty, but it'll get the job done. Normally, I'll end up wrapping the std::map<string, BaseParam*> inside of another class to hide the fact that it's storing a pointers. I like to make my APIs hide the use of pointers as much as possible, it makes it easier for the junior programmers on my team to deal with it.

miked
Never heard of dynamic_cast?
DeadMG
dynamic_cast isn't needed in this situation and it just makes the code harder to read. It's much easier to read (int)i or int(i) rather than dynamic_cast<int>(i).
miked
+2  A: 

It's a tough concept to implement in C++, as you're seeing. I'm always a proponent of using the Boost library, which has already solved it for you. You can typedef the complex boost variant template class to something more usable in your specific domain, so

typedef boost::variant< int, float, bool > ParamT; 
class Param
{
   public:
      // initialize the variants
      Param(ParamT min, ParamT max, ParamT value)
       : m_Min(min), m_Max(max), m_Value(value) {}

      // example accessor
      template<typename OutT>
      const ParamT& value()
      {
          return boost::get<OutT>(m_Value);
      }
      // other accessors for min, max ...
   private:
      ParamT m_Min, m_Value, m_Max;
};



Param speed(-10.0f, 10.0f, 0.0f);
float speedValue = speed.value<float>();

Now, to add another type to your variant (eg, long, std::string, whatever) you can just modify the typedef of ParamT; The catch, here, is that the burden of checking the types is on you - it'll throw an exception if you store a float and try to receive an int, but there's no compile-time safety.

If you want to get really crazy, you can implement an overloaded cast operator on a proxy object....

class ProxyValue
{
public:
  ProxyValue(ParamT& value) : m_Value(value) {}
  template<typename ValueT>
  operator ValueT()
  {
     return boost::get<ValueT>(m_Value);
  }
private:
  ParamT& m_Value;
};

You'd return this from a non-templated value() function in Param, instead of the variant itself. Now you can assign a value without the template call..

Param speed(-10.0f, 0, 10);
float speedValue = speed.value();

Though fair warning, you're stepping into meta-programming hell here. Here thar be dragons. And as always, this is not a complete solution, just a pointer. YMMV.

Heres a roughly working version showing how to use it, and the failures that are easy to hit.

Nicholas M T Elliott
wow this looks perfect thanks. Definitely no point re-inventing the wheel, i'll check out boost::variant.
Memo
i've tried this and I'm really close, but there are a few minor annoyances - with both your code, and my slightly modded version without a proxy ( http://codepad.org/CUHHfYVi ). Your sample code works, but if I try: float a = speed.value() * 2.0f;it doesn't compile complaining that the * operator hasn't been overloaded. I have to write: float a = (float)speed.value() * 2.0f;And I get an assert when i write: int ispeed = (int)speed.value();I have to do: int ispeed = (int)(float)speed.value();are there any tricks to address these?
Memo
Fixing those issues will be more and more work, and make your code more dangerous the more magic you do. That said, the easiest way to deal with the first issue (operator * not defined) is to define it on your class and have it do the smart retrieval. The issue with casting to a different type (if you store a float but want to get it as an integer) is more difficult - basically the variant is throwing away the type information, so you need to specify it to retrieve it. Explicitly casting ( with (int)(float)speed.value() ) will probably save you from shooting yourself in the foot.
Nicholas M T Elliott
A: 

Ok, I'm bored at work (just waiting for something to compile), so here's another solution. Just have one type Param that stores three Values. Those values can by dynamically typed and can store ints and floats (and anything else you want them to).

    class Value
    {
    private:
        union 
        {
            int i,
            float f
        } val;

        DataTypeCode dtc;
    public
        Value() : val.i(0), dtc(INT) {}
        Value(int i) : val.i(i), dtc(INT) {}
        Value(float f) : val.f(f), dtc(FLOAT) {}
        Value& operator=(int i) 
        {
            val.i=i; 
            dtc=INT; 
            return *this;
        }

        Value& operator=(float f) 
        {
            val.f=f; 
            dtc=FLOAT; 
            return *this;
        }

        operator int() 
        {
            switch (dtc)
            {
                case INT: return val.i;
                case FLOAT: return (int)val.f;
            }
            return 0;
        }

        operator float()
        {
            switch (dtc)
            {
                case INT: return (float)val.i;
                case FLOAT: return val.f;
            }
            return 0;
        }
    }

    class Param
    {
    private:
        Value value, min, max 
    public:
        Param(Value value, Value min, Value max) : value(value), min(min), max(max) {}
    }

note, this still requires that DataTypeCode enum that I have in my other answer.

Now to access it, all you have to do is this:

    std::map<string:Param> Params;

    Params["speed"]=Param(1.4,0.1,5.6)

    float speed=Params["speed"]

the cast operators along with the overloaded constructors and operator= functions will automatically convert among the types for you.

miked
+1  A: 

A question I have about your design is why do you need to support all these value types? Performance, type safety, numeric accuracy, or simplicity/ease of use? It's going to be tough to get your interface to support all of these.

One simple way to solve the question, as you posed it, would be to pick a single numeric type that supports all the values you are interested in. In general, a double should suffice. It will be obvious to users what is going on under the hood, and you don't need to do anything weird with your implementation.

If you need perfect storage, you could implement your own numeric type that can do conversions (implicit or explicit) to various numeric types, and maintain perfect storage if you convert to/from the same type. If you're really concerned about perfect storage, you could also make it throw if you try to do a conversion back to the wrong type. This is like a strongly typed union. I believe the boost library has a type like this. Edit: Nicholas M T Elliott's answer already mentions this - boost variant.

If you like the even-more-explicit interface that you have here, with your GetValueAsInt/SetValueAsInt interface, you can still make it slightly simpler. Combine the setters, since C++ supports function overloading for parameters: void SetValue(int value) void SetValue(float value). C++ does not support function overloading for return types, though, so you cannot combine the getters.

Edit:

No matter which of these you pick, you're going to have a problem making it generic, or adding new types to it later. You must modify the property map's value type every time you want to support an new class.

The simplest way around this in C++ is to use a void* as your value type, and do casts to convert it to and from your target type. Your library could provide a template wrapper to do this cast, and throw if the cast fails.

This is similar to using "object" in Java/C#

Edit:

As Michael Aaron Safyan suggested, you could use boost::any.

In the end, you need to think about this: must your design include property dictionaries? If it doesn't have to have it, then you could benefit from the compiler's static analysis if you abandon this idea. Any behavior you push off to runtime will cause bugs that you won't find at compile time. It does make it faster to get the code running, but it makes your runtime error handling harder, and can hurt perf.

Merlyn Morgan-Graham
+1  A: 

You can use either boost::any (to be able to store any type) or boost::variant (to store any type from a fixed set of prespecified types); however, the boost::program_options library largely already does what you want. I would strongly advise that you use boost::program_options rather than rolling this library yourself. I should point out that there is a major downside to what you are doing; you are validating types manually at runtime, which makes it easy for various errors to slip through. I strongly recommend using protocol buffers as a configuration language, as you get stronger type-checking that way.

Michael Aaron Safyan
I recommend this over my answer, unless you can simply refactor your program so you don't need loosly typed property maps. Which is more of a maintenance/development headache in your case: knowing where/how everything is stored at compile time, or pushing that knowledge off to runtime?
Merlyn Morgan-Graham