Gillespie
Gillespie

Reputation: 6561

Generic Type Container implemented with Templates

I have a class that can generically hold any (primitive) type:

class Value
{
    private:
        int             i_value;
        unsigned int    ui_value;
        long            l_value;
        unsigned long   ul_value;
        short           s_value;
        float           f_value;
        double          d_value;
        char            c_value;
        bool            b_value;
        std::string     str_value;

        int type;

        void setValue(int value);
        void setValue(unsigned int value);
        void setValue(long value);
        void setValue(unsigned long value);
        void setValue(short value);
        void setValue(float value);
        void setValue(double value);
        void setValue(char value);
        void setValue(bool value);
        void setValue(std::string value);

    public:
        Value(int value);
        Value(unsigned int value);
        Value(long value);
        Value(unsigned long value);
        Value(short value);
        Value(float value);
        Value(double value);
        Value(char value);
        Value(bool value);
        Value(std::string value);

        Value(Value& other); //Copy Constructor
        ~Value();

        int getType();
        std::string toString(int format);

};

This is nice, because I can do something like:

Value * v1 = new Value(55);
Value * v2 = new Value(1.2);
Value * v3 = new Value("yes");
Value * v4 = new Value(true);

However, as you can see, it's pretty ugly; tons of overloading of everything to make it work. I was thinking templates could make this generic. However, as far as I can tell, you always have to specify the type, which sort of defeats the whole purpose of the class.

For example:

Value<int> * v1 = new Value<int>(55);
Value<double> * v2 = new Value<double>(1.2);
Value<string> * v3 = new Value<string>("yes");
Value<bool> * v4 = new Value<bool>(true);

If I use templates, I can no longer do something like vector<Value *> like I could before. Is this correct, or am I missing some aspect of templates that could help in this situation?

Upvotes: 2

Views: 1001

Answers (3)

Teivaz
Teivaz

Reputation: 5665

All you need is a parent base class for the template one:

class BaseValue
{
public:
    virtual ~BaseValue()
    {}
};

template<typename T>
class Value : public BaseValue
{
public:
    Value(const T& value)
        :m_value(value)
    {}
    void set(const T& value)
    {
        m_value = value; 
    }
    const T& get() 
    {
        return m_value; 
    }
    virtual ~Value()
    {}
private:
    T m_value;
};

std::vector<BaseValue*> values;
values.push_back(new Value<int>(1)); // int
values.push_back(new Value<double>(1.0)); // double
values.push_back(new Value<char*>("asdf")); // pointer to array on stack :(
values.push_back(new Value<char>('c')); // char

Upvotes: 4

mdr
mdr

Reputation: 187

First question: Use a template factory function instead of new.

Second question: Use a common base-class.

In order to be able to properly delete the objects pointed to by the pointers on the vector, you need a virtual destructor. Also, in order to do anything useful with the pointers to the base class, you need virtual methods in the base class.

Example:

class ValueBase
{
public:
    virtual ~ValueBase() = default;
    virtual void Print(std::ostream & os) const = 0;
};

std::ostream & operator<< (std::ostream & os, const ValueBase & value)
{
    value.Print(os);
    return os;
}

template<typename T> class Value : public ValueBase
{
    T value;
public:
    Value(const T & v) : value(v) {}
    const T & Get() const;
    void Set(const T & v);
    void Print(std::ostream & os) const
    {
        os << value;
    }
    // ...
};

template<typename T> Value<T> * NewValue(const T & v)
{
    return new Value<T>(v);
}

Now you can do

ValueBase * v1 = NewValue(55);
ValueBase * v2 = NewValue(1.2);
ValueBase * v3 = NewValue<std::string>("yes");
ValueBase * v4 = NewValue(true);

std::vector<ValueBase *> vec;

vec.push_back(v1);
vec.push_back(v2);
vec.push_back(v3);
vec.push_back(v4);
vec.push_back(NewValue(2350983444325345ll));

for (const auto & entry : vec)
{
    std::cout << *entry << " ";
}
std::cout << "\n";

Note that you normally do not need the template argument of NewValue, since it will be deduced. With "yes" this does not work, since Value<T> will be instantiated with T = char [4] which would require you to use strcpy in the constructor. I find it quite nice to explicity state that a conversion is needed as above. If you prefer this implicitly, do an overload:

Value<std::string> * NewValue(const char * v)
{
    return new Value<std::string>(v);
}

Make sure to delete the contents manually

for (const auto & entry : vec)
{
    delete entry;
}
vec.clear();

or use std::unique_ptr instead of the bare pointer:

template<typename T> std::unique_ptr<Value<T>> UniqueValue(const T & v)
{
    return std::unique_ptr<Value<T>>(new Value<T>(v));
}

std::vector<std::unique_ptr<ValueBase>> vec;
vec.push_back(NewValue(4.5));

If you extend Value and it turns out you need to do something in the destructor, you have to implement the copy constructor, assignment operator and possibly also the move constructor and move assignment operator. ("Rule of Three" or "Rule of Five".) In the above version the "Rule of Zero" still applies, since the destructor is still the same as the implicitly defined one (= default).

If you add the destructor it should be virtual. Otherwise you might get memory leaks if you delete the VirtualBase pointers.

Upvotes: 3

Surt
Surt

Reputation: 16089

Of course you can do templates, but you also need to do polymorphic.

class StupidAndEmpty {} // add virtual destructor if Value needs a destructor

template<class dType>
Value : StupidAndEmpty {
// do smart things with dType
}

vector<StupidAndEmpty *> notSoStupid;

Only problem is how do you use them when you get them back from vector.

Upvotes: 1

Related Questions