davidA
davidA

Reputation: 13664

C++ keeping a collection of pointers to template objects, all derived from a non-template class

I have a list of object "identifiers" (a long enumeration list, with a unique value per "identifier"):

enum Identifier {
  Enum0,  // an identifier for a bool value
  Enum1,  //  ... for a float value
  Enum2,  //  ... for an int value
  // etc.
};

I wish to maintain a collection of Value objects associated with these identifiers. hese Value objects contain a single value, but this value may be integer, floating point, boolean or some other (simple) type. This is in the context of managing a set of configuration values in a system. Later on I plan to extend these value types to support validation of the internal value, and relate some values to other values.

However I wish to use templates for these Value classes, because I want to write operations on these Values generically. If I were to use inheritance I would have BaseValue, then derive IntValue, FloatValue, etc. from BaseValue. Instead I have Value, Value, etc.

But I also want to store an access mechanism to each of these Values in a single collection. I want one class to instantiate all of them and maintain them in the collection. If I were using inheritance, I could use a vector of pointers to BaseValue. But because I'm using templates, these classes are not polymorphically related to each other.

So I thought about making them based on an (empty?) abstract base class that is not parameterised:

class BaseParameter {
};

template<typename T>
class Parameter : public BaseParameter {
 public:
  explicit Parameter(T val) : val_(val) {}
  void set(ParameterSource src) { val_ = extract<T>(src); }
  T get() { return val_; };
 private:
  T val_;
};

Note that the 'set' member function takes a "ParameterSource", which is a source of a value that is 'reinterpreted' by specific "to_type" functions. It's an API function out of my control - I have to interpret the type myself, given that I know what the type is meant to be, set below. That's what extract does - it's specialised for various T types like float, int, bool.

Then I can add them to a std::vector like this:

std::vector<BaseParameter *> vec(10);
vec[Enum0] = new Parameter<bool>(true);   // this is where I state that it's a 'bool'
vec[Enum1] = new Parameter<float>(0.5);   //  ... or a float ...
vec[Enum2] = new Parameter<int>(42);      //  ... or an int ...

I know I should probably use unique_ptr but for now I'm just trying to get this working. So far this seems to work fine. But I'm wary of it because I'm not sure the full type of the instantiated templates is going to be retained at run-time.

Later I want to index the 'vec' by an arbitrary enum value, retrieve the parameter and call a member function on it:

void set_via_source(Identifier id, ParameterSource source) {
  // if id is in range...
  vec[id]->set(source);
}

And other code that makes use of these configuration values (and therefore knows the type) can access them with:

int foo = vec[Enum2]->get() * 7;

This seemed to work, most of the time. It compiles. I've had some odd crashes I can't explain, that tend to crash the debugger too. But I'm very suspicious of it, because I don't know whether C++ is able to determine the real type of the pointed-to object (including the parameterised type), because the base class isn't parameterised itself.

Unfortunately it seems to me that if I parameterise the base class, then I essentially remove the commonality between these Value classes that allow them to be stored in a single container.

I took a look at boost::any to see if that might help, but I'm not sure it would apply in this case.

At a higher level, what I'm trying to do is connect a vast collection of configuration items from an external source (via an API) that delivers values of different type depending on the item, and store them locally so that the rest of my code can easily access them as if they are simple data members. I also want to avoid writing a giant switch statement (because that would work).

Is this something that Type Erasure might help me with?

Upvotes: 0

Views: 508

Answers (1)

rici
rici

Reputation: 241741

If you know at compile time the type associated with each enum, you can do this "easily" with boost::variant and without type-erasure or even inheritance. (Edit: The first solution uses a lot of C++11 features. I put a less-automatic but C++03 conformant solution at the end.)

#include <string>
#include <vector>
#include <boost/variant.hpp>
#include <boost/variant/get.hpp>

// Here's how you define your enums, and what they represent:
enum class ParameterId {
  is_elephant = 0,
  caloric_intake,
  legs,
  name,
  // ...
  count_
};
template<ParameterId> struct ConfigTraits;

// Definition of type of each enum
template<> struct ConfigTraits<ParameterId::is_elephant> {
  using type = bool;
};
template<> struct ConfigTraits<ParameterId::caloric_intake> {
  using type = double;
};
template<> struct ConfigTraits<ParameterId::legs> {
  using type = int;
};
template<> struct ConfigTraits<ParameterId::name> {
  using type = std::string;
};
// ...

// Here's the stuff that makes it work.

class Parameters {
  private:
    // Quick and dirty uniquifier, just to show that it's possible
    template<typename...T> struct TypeList {
      using variant = boost::variant<T...>;
    };

    template<typename TL, typename T> struct TypeListHas;
    template<typename Head, typename...Rest, typename T>
    struct TypeListHas<TypeList<Head, Rest...>, T>
        : TypeListHas<TypeList<Rest...>, T> {
    };
    template<typename Head, typename...Rest>
    struct TypeListHas<TypeList<Head, Rest...>, Head> {
      static const bool value = true;
    };
    template<typename T> struct TypeListHas<TypeList<>, T> {
      static const bool value = false;
    };

    template<typename TL, typename T, bool B> struct TypeListMaybeAdd;
    template<typename TL, typename T> struct TypeListMaybeAdd<TL, T, false> {
      using type = TL;
    };
    template<typename...Ts, typename T>
    struct TypeListMaybeAdd<TypeList<Ts...>, T, true> {
      using type = TypeList<Ts..., T>;
    };
    template<typename TL, typename T> struct TypeListAdd
        : TypeListMaybeAdd<TL, T, !TypeListHas<TL, T>::value> {
    };

    template<typename TL, int I> struct CollectTypes
        : CollectTypes<typename TypeListAdd<TL,
                                            typename ConfigTraits<ParameterId(I)>::type
                                           >::type, I - 1> {
    };
    template<typename TL> struct CollectTypes<TL, 0> {
      using type = typename TypeListAdd<TL,
                                        typename ConfigTraits<ParameterId(0)>::type
                                       >::type::variant;
    };

  public:
    using value_type =
        typename CollectTypes<TypeList<>, int(ParameterId::count_) - 1>::type;

    template<ParameterId pid>
    using param_type = typename ConfigTraits<pid>::type;

    // It would be better to not initialize all the values twice, but this
    // was easier.
    Parameters() : values_(size_t(ParameterId::count_)) {
       clear(std::integral_constant<int, int(ParameterId::count_) - 1>());
    }

    // getter for when you know the id at compile time. Should have better
    // error checking.
    template<ParameterId pid>
    typename ConfigTraits<pid>::type get() {
      // The following will segfault if the value has the wrong type.
      return *boost::get<typename ConfigTraits<pid>::type>(&values_[int(pid)]);
    }

    // setter when you know the id at compile time
    template<ParameterId pid>
    void set(typename ConfigTraits<pid>::type new_val) {
      values_[int(pid)] = new_val;
    }

    // getter for an id known only at runtime; returns a boost::variant;
    value_type get(ParameterId pid) {
      return values_[int(pid)];
    }

  private:
    // Initialize parameters to default values of the correct type
    template<int I> void clear(std::integral_constant<int, I>) {
       values_[I] = param_type<ParameterId(I)>();
       clear(std::integral_constant<int, I - 1>());
    }
    void clear(std::integral_constant<int, 0>) {
      values_[0] = param_type<ParameterId(0)>();
    }

    std::vector<value_type> values_;
};

// And finally, a little test
#include <iostream>
int main() {
  Parameters parms;
  std::cout << ('(' + parms.get<ParameterId::name>() + ')')<< ' '
            << parms.get<ParameterId::is_elephant>() << ' '
            << parms.get<ParameterId::caloric_intake>() << ' '
            << parms.get<ParameterId::legs>() << std::endl;
  parms.set<ParameterId::is_elephant>(true);
  parms.set<ParameterId::caloric_intake>(27183.25);
  parms.set<ParameterId::legs>(4);
  parms.set<ParameterId::name>("jumbo");
  std::cout << ('(' + parms.get<ParameterId::name>() + ')')<< ' '
            << parms.get<ParameterId::is_elephant>() << ' '
            << parms.get<ParameterId::caloric_intake>() << ' '
            << parms.get<ParameterId::legs>() << std::endl;

  return 0;
}

For the benefit of those who can't yet use C++11, here's a version which uses non-class enums and which is not smart enough to build the boost::variant type by itself, so you have to provide it manually:

#include <string>
#include <vector>
#include <boost/variant.hpp>
#include <boost/variant/get.hpp>

// Here's how you define your enums, and what they represent:
struct ParameterId {
  enum Id {
    is_elephant = 0,
    caloric_intake,
    legs,
    name,
    // ...
    count_
  };
};
template<int> struct ConfigTraits;

// Definition of type of each enum
template<> struct ConfigTraits<ParameterId::is_elephant> {
  typedef bool type;
};
template<> struct ConfigTraits<ParameterId::caloric_intake> {
  typedef double type;
};
template<> struct ConfigTraits<ParameterId::legs> {
  typedef int type;
};
template<> struct ConfigTraits<ParameterId::name> {
  typedef std::string type;
};
// ...

// Here's the stuff that makes it work.

// C++03 doesn't have integral_constant, so we need to roll our own:
template<int I> struct IntegralConstant { static const int value = I; };

template<typename VARIANT>
class Parameters {
  public:
    typedef VARIANT value_type;

    // It would be better to not initialize all the values twice, but this
    // was easier.
    Parameters() : values_(size_t(ParameterId::count_)) {
       clear(IntegralConstant<int(ParameterId::count_) - 1>());
    }

    // getter for when you know the id at compile time. Should have better
    // error checking.
    template<ParameterId::Id pid>
    typename ConfigTraits<pid>::type get() {
      // The following will segfault if the value has the wrong type.
      return *boost::get<typename ConfigTraits<pid>::type>(&values_[int(pid)]);
    }

    // setter when you know the id at compile time
    template<ParameterId::Id pid>
    void set(typename ConfigTraits<pid>::type new_val) {
      values_[int(pid)] = new_val;
    }

    // getter for an id known only at runtime; returns a boost::variant;
    value_type get(ParameterId::Id pid) {
      return values_[int(pid)];
    }

  private:
    // Initialize parameters to default values of the correct type
    template<int I> void clear(IntegralConstant<I>) {
      values_[I] = typename ConfigTraits<I>::type();
      clear(IntegralConstant<I - 1>());
    }
    void clear(IntegralConstant<0>) {
      values_[0] = typename ConfigTraits<0>::type();
    }

    std::vector<value_type> values_;
};

// And finally, a little test
#include <iostream>
int main() {
  Parameters<boost::variant<bool, int, double, std::string> > parms;
  std::cout << ('(' + parms.get<ParameterId::name>() + ')')<< ' '
            << parms.get<ParameterId::is_elephant>() << ' '
            << parms.get<ParameterId::caloric_intake>() << ' '
            << parms.get<ParameterId::legs>() << std::endl;
  parms.set<ParameterId::is_elephant>(true);
  parms.set<ParameterId::caloric_intake>(27183.25);
  parms.set<ParameterId::legs>(4);
  parms.set<ParameterId::name>("jumbo");
  std::cout << ('(' + parms.get<ParameterId::name>() + ')')<< ' '
            << parms.get<ParameterId::is_elephant>() << ' '
            << parms.get<ParameterId::caloric_intake>() << ' '
            << parms.get<ParameterId::legs>() << std::endl;

  return 0;
}

Upvotes: 1

Related Questions