开发者

c++: heterogeneous template for bounded numbers

I want to create a list (std::container or even list* would be ok) of numbers (int and double) that can have limits imposed on them.

template<typename T> 
class setting {
  public:
    std::string name, units;
    T value, min, max, step;

    setting(std::string n, T val) : name(n), value(val) { }

    setting operator++(int) {
      setting tmp = *this;
      value += step; if(value > max) value = max;
      return tmp;
    }
};
...
list.add(new setting<int>("channel", 4));
list.add(new setting<double>("amplitude", 5.6));
...
for(int i = 0; i < list.size(); i++)
  std::cout << list[i].name << ": " << list[i].value << std::endl;

I've tried this a couple of different ways but I'm not happy with any of them. Can't derive from a common base because the base type doesn't know about `value' be开发者_如何学Ccause it doesn't know the type or has to have the type defined ahead of time. Tried it with macro templates but the downcast feels sloppy. Is there a way to do this without resorting to a union of all types with a type identifier to select the right member?

boost::variant with overloaded constructors seems to do what I want so far, except for having to enumerate all the types at compile time:

class setting {
   public:
      boost::variant<
         bool,
         int8_t,
         uint8_t,
         int16_t,
         uint16_t,
         int32_t,
         uint32_t,
         int64_t,
         uint64_t,
         float,
         double,
         std::string
            > value;

      std::string name;

      setting(std::string n, int v) : name(n), value(v) { }
      setting(std::string n, double v) : name(n), value(v) { }
      setting(std::string n, std::string v) : name(n), value(v) { }
};

typedef std::map<std::string, setting*> MapType;
typedef MapType::const_iterator MapItr;

int main() {
   MapType settinglist;

   settinglist["height"] = new setting("height", 1.3);
   settinglist["width"]  = new setting("width", 5);
   settinglist["name"]   = new setting("name", "the name");

   for(MapItr i = settinglist.begin(); i != settinglist.end(); ++i) {
      std::cout << i->second->name
         << " : " << i->second->value
         << std::endl;
   }

   return 0;
};

gives:

height : 1.3
name : the name
width : 5


Maybe a common base class with virtual toString and fromString functions? Then your for loop becomes:

list<setting_base> lst;
for( list<setting_base>::iterator it = lst.begin(); it != lst.end(); ++it )
     std::cout << it->name << ": " << it->toString() << std::endl;


How about wrapping your plain type in Boost.Operators?

template <class T, T max = numeric_limits<T>::max(), T min = 0>
class Saturate
    : boost::operators<Saturate<T, max, min>, T >
{
private:
    T _value;
    void normalize() {
        if(_value < min) _value = min;
        if(_value > max) _value = max;
    }
    void toNormal(T t) {
        if(t < min) return min;
        if(t > max) return max;
        return t;
    }
public:
    Saturate(T t = T()) : _value(toNormal(t)) {}
    T value() { return _value; }
    Saturate& operator+=(const Saturate& x)
      { _value += x._value; normalize(); return *this; }
    Saturate& operator-=(const Saturate& x)
      { _value -= x._value; normalize(); return *this; }
    ...
};
...
std::vector<Saturate<int, 1023, -1023> > volume;
...
volume[3] = 50000; // really loud
std::cout << volume[3].value(); // Not so loud


I think you'll have to either fit everything into one type (meaning forget about int, just use double) or, define a more generic base type.

The problem that you noted with the generic base type is that it's, well generic. You're losing type information that you can't later get back when your accessing the elements in the container.

I think that your design goal is inconsistent. You should either create a generic interface that's good enough for your needs (that's type agnostic) or pick one type and stick to it.


I'll start by saying you could just provide a few toInt and toDouble and toInt64 methods in your base class. Then, if you need the double value, you just ask for it. It requires a minimal amount of effort on everybody's part.

Failing that, you could replace a regular virtual int value() const method with a virtual void value(Operator&) const one. Operator would provide virtual functions of its own that each accept one of the types you would want to act on. Essentially:

struct Operator {
    virtual act(int) = 0;
    virtual act(double) = 0;
    //repeat for other types
};

struct Base {
    virtual value(Operator&) const = 0;
};

When you call value on your base type, virtual dispatch will ensure the correct implementation gets called. Within that implementation, you provide the proper static type to act, and overload resolution kicks in. You can actually perform your calculations within the Operator instance, or just store the result and provide an accessor. In the former case, you can do something unique for each type, like use an algorithm that works faster with integer types but without the precision. In the later case, you can go one step further and provide a templated accessor within Base

struct Base {
    //other stuff
    template<typename Operator>
    typename Operator::ResultType value() const {
        Operator op;
        value(op); 
        return op.result();
    }
}
// later
cout << basePtr->get<IntGetter>();

Of course, the end result of that is an extremely convoluted way of doing what I initially suggested.

/////// I just noticed your edit to the original question. With so many possible base types, this becomes much less feasible. You have to provide overloads of each primitive type within your Operator classes. You could provide default implementations to the base Operator class; like calling act(int) for all integral types and act(double) for all floating point types, then you'd be back down to just two required overloads per implementation, plus whatever additional ones you need for that specific use case.

But now I'm looking towards YAGNI. You've got a complicated base class, just so you can provide settings that save a few bytes by not storing a full int? Can you really not just store everything as a double? That stores a whole bunch of integer values precisely. Or use boost::variant and restrict yourself to ints, doubles, and strings.

0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜