Watusimoto
Watusimoto

Reputation: 1888

Templates, Inheritance, and Virtual Methods (C++)

I am trying to write a generic settings manager. Settings come from an INI file and can be integer or string. I want to be able to store a vector of these settings, so I can iterate through them to find the desired one, and extract its value.

I want to be able to write something like this:

// Desired usage:
Settings settings;      // A container class, defined below
settings.add(new Setting<string>("shipName", "HAL"));
settings.add(new Setting<int>   ("shipYear", 2001));

// Different return types:
string shipName = settings.getSetting("shipName")->getValue(); 
int shipYear    = settings.getSetting("shipYear")->getValue();

I've got 3 classes:

  1. AbstractSetting, which is the mother of all setting classes. I need this so I can have a consistent type to store in a vector.

  2. Setting, a templated class that inherits from AbstractSetting. Here I can store the setting data as either string or integer.

  3. Settings, a container class for holding my settings, which takes care of storage and retrieval.

Methods are mostly getters/setters. Since implementations are obvious, I've omitted them for the sake of brevity.

My question is what do I put in AbstractSetting to allow me to have different implementations (with different return types) for getValue()?

class AbstractSetting
{
private:
   string mName;

public:
   AbstractSetting(const string &name);     // Constructor

   // What does here?  Need to declare getValue somehow
};

////////////////////////////////////////

// Sublcasses of AbstractSetting, one for each T
template <class T>
class Setting : public AbstractSetting
{
private:
   T mValue;

public:
   Setting<T>(const string &name, const T &value);

   void setValue(const T &value);
   T getValue();
};

////////////////////////////////////////

// Container for all our settings
class Settings 
{
private:
   Vector<AbstractSetting *> mSettings;

public: 
   const AbstractSetting *getSetting(const string &name) const;
   void add(AbstractSetting *setting);    // Store new setting
};

Upvotes: 2

Views: 258

Answers (3)

iavr
iavr

Reputation: 7637

Here's another solution:

#include <iostream>
#include <map>
#include "some.hpp"

int main(int argc, char *argv[])
{
    using namespace std;

    map <string, some> settings;
    settings["shipName"] = string("HAL");
    settings["shipYear"] = 2001;

    string shipName = settings["shipName"];
    int    shipYear = settings["shipYear"];

    cout << shipName << " " << shipYear << endl;
}

Careful, data must be retrieved by exactly the same type as when stored. This is why I used string("HAL"); plain "HAL" would need const char* for shipName.

The key to this solution is class ivl::some, that is part of library ivl and works similarly to boost::any, but is more efficient in using the stack when data can fit into a predetermined size.

In fact, boost::any uses more or less the same method as you do, always placing data on the heap. Its source code is quite small and it is more relevant to your solution, so it may help as well.

Instead of using a non-template base class along with template derived classes with virtual methods, some uses a single pointer to function that supports copy or delete operations, depending on whether the 2nd argument is zero.

Because this function is an instantiation of a static template method, the data type is known within the body of the function, as well as whether heap or stack memory is used. This affects allocation (new or placement new) and de-allocation (delete or plain destructor call). The decision is solely based on the size of the data.

Data access is simply done by static_cast and requires the user to specify a type, either explicitly via method _<T>(), or implicitly via a conversion operator to T& or const T&, where template argument T is automatically deduced. These are similar to the previous answers, but read-write access is also supported via the T& conversion.

A stripped down version of some.hpp that is enough for this example takes 100 lines of code:

//-----------------------------------------------------------------------------

template <typename T>       void* away(T*       p) { return static_cast <void*>      (p); }
template <typename T> const void* away(const T* p) { return static_cast <const void*>(p); }
template <typename T>       void* ref (T&       r) { return away(&r); }
template <typename T> const void* ref (const T& r) { return away(&r); }

template <typename T>       T* back (void*       p) { return static_cast <T*>      (p); }
template <typename T> const T* back (const void* p) { return static_cast <const T*>(p); }
template <typename T>       T& deref(void*       p) { return *back <T>(p); }
template <typename T> const T& deref(const void* p) { return *back <T>(p); }

inline void*       peek(void*       p) { return deref <void*>      (p); }
inline const void* peek(const void* p) { return deref <const void*>(p); }

//-----------------------------------------------------------------------------

enum { stack_size = 8 };

template <int N = stack_size>
class some_
{
    union { char b[N]; void* p; };   // buffer; pointer
    void (*f)(void*&, const void*);  // operations

//-----------------------------------------------------------------------------

    template <typename T>
    static void stack(void*& dest, const void* src)
    {
        if (src) new (dest) T(deref <T>(src));
        else back <T>(ref(dest))->~T();
    };

    template <typename T>
    static void heap(void*& dest, const void* src)
    {
        if (src) dest = new T(deref <T>(peek(src)));
        else delete back <T>(dest);
    };

//-----------------------------------------------------------------------------

    template <typename T> bool fits() { return sizeof(T) <= N; }

    void read() { f = 0; }

    template <typename T>
    void read(const T& v)
    {
        fits <T>() ? new (b) T(v) : p = new T(v);
        f = fits <T>() ? stack <T> : heap <T>;
    }

    void read(const some_& s) { if ((f = s.f)) (*f)(p = b, s.b); }
    void free()               { if ( f )       (*f)(p, 0); }

    template <typename T>       void* ptr()       { return fits <T>() ? away(b) : p; }
    template <typename T> const void* ptr() const { return fits <T>() ? away(b) : p; }

protected:

//-----------------------------------------------------------------------------

                          some_& assign()           { free(); read();  return *this; }
    template <typename T> some_& assign(const T& v) { free(); read(v); return *this; }

public:

//-----------------------------------------------------------------------------

     some_() { read(); }
    ~some_() { free(); }

                          some_(const some_& s) { read(s); }
    template <typename T> some_(const T& v)     { read(v); }
    template <typename T> some_(const T  v[])   { read <const T*>(v); }

                          some_& operator=(const some_& s) { return assign(s); }
    template <typename T> some_& operator=(const T& v)     { return assign(v); }
    template <typename T> some_& operator=(const T  v[])   { return assign <const T*>(v); }

    some_& init()  { return assign(); }
    some_& clear() { return assign(); }

    bool empty()      const { return f == 0; }
    bool operator()() const { return f != 0; }

    template <typename T>       T* to()       { return back <T>(ptr <T>()); }
    template <typename T> const T* to() const { return back <T>(ptr <T>()); }

    template <typename T>       T& _()       { return *to <T>(); }
    template <typename T> const T& _() const { return *to <T>(); }

    template <typename T> operator       T&()       { return _<T>(); }
    template <typename T> operator const T&() const { return _<T>(); }
};

//-----------------------------------------------------------------------------

typedef some_<> some;

//-----------------------------------------------------------------------------

Data may be safely copied between one some and another, without specifying the type. Large stored data may be accessed through references or directly modified, but then the type is required. E.g.

string& shipName = settings["shipName"];

would enable modifications like shipName += ... within the settings container.

Assigning a pointer or C array to a some only copies the pointer, not the actual data. C arrays are stored as pointers, so the corresponding pointer type must specified on retrieval.

It is safe to clear() a some, properly desctructing the stored object even if it resides on the stack, and possible to know whether it is empty(). No type is needed for these operations.

The size of stored data may be controlled, but uniformly. E.g., if you are willing to give 32 bytes to every stored item and allocate space on the heap only for larger ones, you can define instead

typedef some_<32> some;

The default is 8 bytes, which shared with a pointer within a union. This is the minimum possible, on 64-bit. For types of zero size, a specialized stack-only version of some would be needed. some also contains a pointer to function, so its size is typically 16 bytes.

Nested containers of some are possible, making up e.g. trees of heterogeneous data.

The full version supports testing whether a given type will succeed, or whether e.g. two stored types are equal, using a special kind of type_id. But it is not possible to recover types automatically or make conversions.

A more advanced structure, some_of, does not need to specify the data types at all upon retrieval, but works only with a specified type of destination for the data, e.g. ostream. This would work like this:

map <string, some_of <ostream> > settings;
settings["shipName"] = string("HAL");
settings["shipYear"] = 2001;

cout << settings["shipName"] << " " << settings["shipYear"] << endl;

To achieve this, some_of contains one more pointer to function.

Using custom data types and custom destinations, the same idea may be applied to building e.g. a message queue for deferred function calls.

Upvotes: 0

Watusimoto
Watusimoto

Reputation: 1888

I'll offer one other solution I've found, though I'm not yet sure if I like it... This was adapted from: Why can't C++ deduce template type from assignment?

By adding this to the AbstractSettings class:

template<class T>
operator T()
{
   return getValue<T>();
}

I can override C++'s implicit casting and make this syntax work:

int shipYear = *settings.getSetting("shipYear");

Upvotes: 1

Uman
Uman

Reputation: 131

I think you will have to tell the compiler what type you expect, you can do this by doing something like this:

class AbstractSetting
{
  private:
   string mName;

  public:
   AbstractSetting(const string &name);     // Constructor

   template <typename T>
   T&  getTheValue()
   {
      Settings<T>* upcast = dynamic_cast<Settings<T>*>(this);
      if (!upcast)
         ; //throw your exception
      return upcast->getValue();
   }

   template <typename T>
   T const&  getTheValue() const
   {
      Settings<T>* upcast = dynamic_cast<Settings<T>*>(this);
      if (!upcast)
         ; //throw your exception
      return upcast->getValue();
   }
};

and calling it with :

 string & value = settings.getSettings("strName").getTheValue<string>();
 int otherValue = settings.getSettings("intName").getTheValue<int>();

If you do not want to specify the return type, you can pass a variable by reference, by doing something like this:

class AbstractSetting
{
  private:
   string mName;

  public:
   AbstractSetting(const string &name);     // Constructor

   template <typename T>
   void  getTheValue(T& ret)
   {
      Settings<T>* upcast = dynamic_cast<Settings<T>*>(this);
      if (!upcast)
         ; //throw your exception
      ret = upcast->getValue();
   }
};

And use it like:

 string value;
 int otherValue;
 settings.getSettings("stringName").getTheValue(value); // will do a copy, so this is kind of bad, avoid this by using pointer.
 settings.getSettings("intName").getTheValue(otherValue);

Upvotes: 2

Related Questions