3
\$\begingroup\$

I made a triple-type container, that is, a single type that can store a value, under one of the three types used. It's somewhat similar to Boost::Variant.

The header file (multitype.hpp):

#ifndef MULTITYPE_HPP
#define MULTITYPE_HPP

#include <iostream>
#include <cstdlib>
#include <typeinfo>

typedef void* vp_t; // Void pointer (can contain every type)

template <typename _A,typename _B> struct is_same_type  { const static bool value = false; };
template <typename _A> struct is_same_type<_A,_A>       { const static bool value = true;  };

template <class A,class B,class C>
class TripleType
{
    protected:
        enum ct_t { _tA, _tB, _tC, _init_ }; // Type flags
        struct abc_t
        {
            A __a;         // A container
            B __b;         // B container
            C __c;         // C container
            ct_t _e; // Which type is the current one?
        };
    private:
        abc_t val;
    public:
        TripleType()
        {
            (this->val)._e = _init_;
        }
        template <class T> TripleType(T x)
        {
            vp_t yv; yv = &x;
            bool is_A = is_same_type<T,A>::value;
            bool is_B = is_same_type<T,B>::value;
            bool is_C = is_same_type<T,C>::value;
            if      (is_A) { A *t = static_cast<A*>(yv); val.__a = *t; val._e = _tA; }
            else if (is_B) { B *t = static_cast<B*>(yv); val.__b = *t; val._e = _tB; }
            else if (is_C) { C *t = static_cast<C*>(yv); val.__c = *t; val._e = _tC; }
        }
        template <class T> TripleType& operator=(T rhs)
        {
            vp_t yv = &rhs;
            bool is_A = is_same_type<T,A>::value;
            bool is_B = is_same_type<T,B>::value;
            bool is_C = is_same_type<T,C>::value;
            if      (is_A) { A *t = static_cast<A*>(yv); (this->val).__a = *t; (this->val)._e = _tA; }
            else if (is_B) { B *t = static_cast<B*>(yv); (this->val).__b = *t; (this->val)._e = _tB; }
            else if (is_C) { C *t = static_cast<C*>(yv); (this->val).__c = *t; (this->val)._e = _tC; }
            return *this;
        }
        template <class T> T get()
        {
            vp_t yv;
            bool is_A = is_same_type<T,A>::value;
            bool is_B = is_same_type<T,B>::value;
            bool is_C = is_same_type<T,C>::value;
            if      (is_A) yv = &(val.__a);
            else if (is_B) yv = &(val.__b);
            else if (is_C) yv = &(val.__c);
            T *t = static_cast<T*>(yv);
            return *t;
        }
        const std::type_info& current_type()
        {
            if      ((this->val)._e == _tA)
                return typeid(A);
            else if ((this->val)._e == _tB)
                return typeid(B);
            else if ((this->val)._e == _tC)
                return typeid(C);
        }
};

#endif

Here is an usage example:

#include <iostream>
#include <cstdlib>
#include <string>
#include "multitype.hpp" // The header file where I wrote the mentioned container

using namespace std;

int main()
{
    srand(time(NULL));
    TripleType<int,string,float> test;
    test = rand();
    cout << test.get<int>() << endl;
    test = string("TripleType test");
    cout << test.get<string>() << endl;
    test = float(1.6108);
    cout << test.get<float>() << endl;
    test = 2016;
    cout << test.get<int>() << endl;
    test = string("Bye.");
    cout << test.get<string>() << endl;
}

Any improvements, suggestions, etc. are welcome.

\$\endgroup\$
  • 1
    \$\begingroup\$ When you are making a variant type, it is better to use a union than a struct. That's how you get the space savings. \$\endgroup\$ – Chris Beck Jul 3 '16 at 7:17
2
\$\begingroup\$
  1. Don't make it possible for your container to contain nothing. It can be wrapped in a std::optional for that. Drop the _init_ type and parameterless constructor.

  2. Use val consistently. The parentheses around this->val are unnecessary anyway, but since you're already using val in some places, just use it everywhere.

  3. Don't allow construction or assignment with values of bad types. That fails silently right now, which is not a good way to fail. Luckily, you can do even better and keep the static typing with overloading:

    TripleType(A x) { val._e = _tA; val.__a = x; }
    TripleType(B x) { val._e = _tB; val.__b = x; }
    TripleType(C x) { val._e = _tC; val.__c = x; }
    
    TripleType& operator=(A x) { val._e = _tA; val.__a = x; return *this; }
    TripleType& operator=(B x) { val._e = _tB; val.__b = x; return *this; }
    TripleType& operator=(C x) { val._e = _tC; val.__c = x; return *this; }
    
  4. Mark methods that don't modify their object – get() and current_type() – as const.

  5. Don't allow fetching values of the wrong type. I'm not a C++ expert by any means, so there might be a better way of restricting T to A, B, or C while keeping the same .get<T> calling convention (I sure hope so), but:

    private:
        A get_(A const*) const {
            if (val._e != _tA) {
                throw bad_multitype_access(typeid(A), current_type());
            }
    
            return val.__a;
        }
    
        B get_(B const*) const {
            if (val._e != _tB) {
                throw bad_multitype_access(typeid(B), current_type());
            }
    
            return val.__b;
        }
    
        C get_(C const*) const {
            if (val._e != _tC) {
                throw bad_multitype_access(typeid(C), current_type());
            }
    
            return val.__c;
        }
    
    public:
        template <class T> T get() const {
            return get_(static_cast<T const*>(nullptr));
        }
    

    where bad_multitype_access would be something along the lines of:

    class bad_multitype_access : std::logic_error {
    public:
        bad_multitype_access(std::type_info const& expected, std::type_info const& actual) :
            std::logic_error(std::string("Attempted to get ") + expected.name() + " from multitype containing " + actual.name()) {}
    };
    

    That's all verbose and a bit hacky, but prevents getting types outside of the expected set statically and values of the wrong type dynamically. Again, I would love to hear a better way of accomplishing the same thing from someone who actually knows the language. You can also drop the vp_t typedef at this point, which was never really a good idea, as well as is_same_type.

  6. Use a switch for current_type().

    std::type_info const& current_type() const {
        switch (val._e) {
        case _tA: return typeid(A);
        case _tB: return typeid(B);
        case _tC: return typeid(C);
        }
    }
    
  7. Stop it with the underscores already! =)

    private:
        enum contained_type {
            tA,
            tB,
            tC,
        };
    
        struct {
            A a;
            B b;
            C c;
            contained_type type;
        } val;
    
  8. It feels like creating a TripleType shouldn't create an instance of each of its possible types, and that writing a new value of a different type should destroy a previously existing one. unique_ptrs could work here.

    private:
        ⋮
        struct {
            std::unique_ptr<A> a;
            std::unique_ptr<B> b;
            std::unique_ptr<C> c;
            contained_type type;
        } val;
        ⋮
            return *val.a;
        ⋮
            return *val.b;
        ⋮
            return *val.c;
    
    public:
        TripleType(A x) { val.type = tA; val.a = std::make_unique<A>(x); }
        TripleType(B x) { val.type = tB; val.b = std::make_unique<B>(x); }
        TripleType(C x) { val.type = tC; val.c = std::make_unique<C>(x); }
    
        TripleType& operator=(A x) {
            val.type = tA;
            val.a = std::make_unique<A>(x);
            val.b.reset(nullptr);
            val.c.reset(nullptr);
            return *this;
        }
    
        TripleType& operator=(B x) {
            val.type = tB;
            val.a.reset(nullptr);
            val.b = std::make_unique<B>(x);
            val.c.reset(nullptr);
            return *this;
        }
    
        TripleType& operator=(C x) {
            val.type = tC;
            val.a.reset(nullptr);
            val.b.reset(nullptr);
            val.c = std::make_unique<C>(x);
            return *this;
        }
    
  9. Avoid the double-copy-construct by accepting T const& instead of T in the constructors and operator=s.

  10. Since TripleType is managing everything about val, move val's members into it; it's just doing namespacing right now and not representing an actual object.

  11. Give TripleType some copy and move constructors.

    TripleType(TripleType<A, B, C> const& o) {
        type = o.type;
    
        switch (type) {
        case tA: a = std::make_unique<A>(*o.a); break;
        case tB: b = std::make_unique<B>(*o.b); break;
        case tC: c = std::make_unique<C>(*o.c); break;
        }
    }
    
    TripleType(TripleType<A, B, C>&& o) = default;
    

All told, it looks like this now:

#ifndef MULTITYPE_HPP
#define MULTITYPE_HPP

#include <memory>
#include <stdexcept>

class bad_multitype_access : std::logic_error {
public:
    bad_multitype_access(std::type_info const& expected, std::type_info const& actual) :
        std::logic_error(std::string("Attempted to get ") + expected.name() + " from multitype containing " + actual.name()) {}
};

template <class A, class B, class C>
class TripleType {
private:
    enum contained_type {
        tA,
        tB,
        tC,
    };

    std::unique_ptr<A> a;
    std::unique_ptr<B> b;
    std::unique_ptr<C> c;
    contained_type type;

    A get_(A const*) const {
        if (type != tA) {
            throw bad_multitype_access(typeid(A), current_type());
        }

        return *a;
    }

    B get_(B const*) const {
        if (type != tB) {
            throw bad_multitype_access(typeid(B), current_type());
        }

        return *b;
    }

    C get_(C const*) const {
        if (type != tC) {
            throw bad_multitype_access(typeid(C), current_type());
        }

        return *c;
    }

public:
    TripleType(TripleType<A, B, C> const& o) {
        type = o.type;

        switch (type) {
        case tA: a = std::make_unique<A>(*o.a); break;
        case tB: b = std::make_unique<B>(*o.b); break;
        case tC: c = std::make_unique<C>(*o.c); break;
        }
    }

    TripleType(TripleType<A, B, C>&& o) = default;

    TripleType(A const& x) { type = tA; a = std::make_unique<A>(x); }
    TripleType(B const& x) { type = tB; b = std::make_unique<B>(x); }
    TripleType(C const& x) { type = tC; c = std::make_unique<C>(x); }

    TripleType& operator=(A const& x) {
        type = tA;
        a = std::make_unique<A>(x);
        b.reset(nullptr);
        c.reset(nullptr);
        return *this;
    }

    TripleType& operator=(B const& x) {
        type = tB;
        a.reset(nullptr);
        b = std::make_unique<B>(x);
        c.reset(nullptr);
        return *this;
    }

    TripleType& operator=(C const& x) {
        type = tC;
        a.reset(nullptr);
        b.reset(nullptr);
        c = std::make_unique<C>(x);
        return *this;
    }

    template <class T> T get() const {
        return get_(static_cast<T const*>(nullptr));
    }

    std::type_info const& current_type() const noexcept {
        switch (type) {
        case tA: return typeid(A);
        case tB: return typeid(B);
        case tC: return typeid(C);
        }
    }
};
#endif

And now there's more type safety. (I hope this isn't actually egregiously bad advice!)

Also, here are some moving constructors and operator=s you can add.

TripleType(A&& x) { type = tA; a = std::make_unique<A>(std::move(x)); }
TripleType(B&& x) { type = tB; b = std::make_unique<B>(std::move(x)); }
TripleType(C&& x) { type = tC; c = std::make_unique<C>(std::move(x)); }

TripleType& operator=(A&& x) {
    type = tA;
    a = std::make_unique<A>(std::move(x));
    b.reset(nullptr);
    c.reset(nullptr);
    return *this;
}

TripleType& operator=(B&& x) {
    type = tB;
    a.reset(nullptr);
    b = std::make_unique<B>(std::move(x));
    c.reset(nullptr);
    return *this;
}

TripleType& operator=(C&& x) {
    type = tC;
    a.reset(nullptr);
    b.reset(nullptr);
    c = std::make_unique<C>(std::move(x));
    return *this;
}
\$\endgroup\$
  • \$\begingroup\$ ad 9, if you use std::move() then passing the arguments by value is not that bad idea at all. \$\endgroup\$ – wilx Jul 3 '16 at 8:43
  • \$\begingroup\$ Wow, this is great! Also, I've never thought about using switches. Thanks. \$\endgroup\$ – sator.arepo.tenet.opera.rotas Jul 3 '16 at 19:56
1
\$\begingroup\$

Unions

You should use a tagged union instead of storing all three types. That's how boost::variant works and that's the best approach since the container is storing only one of the three types at any time.

Starting with C++11, we can declare unrestricted unions, that is, it doesn't matter if the object is Plain old Data (POD) or has a constructor/destructor.

enum class Type { TA, TB, TC };
union Data
{
    A asA;
    B asB;
    C asC;
};

Data data; // The raw data
Type type; // The "tag"

The union Data will only use enough space to store the largest of the three values, so if you have <int, std::string, float>, sizeof(Data) == sizeof(std::string). (assuming string is the largest of the three).

With a union, you're responsible for constructing and destroying the stored value though, so for instance, when assigning or initializing we have to invoke the constructor with placement new:

template <class T> TripleType& operator = (const T& rhs)
{
    // Note should first destroy the current value if necessary
    // E.g.: data.asA.~T();

    switch (type)
    {
    case Type::TA :
        ::new(&data.asA) T(rhs);
        type = Type::TA;
        break;
        ...
    }
    ...
}

Similarly, in ~TripleType() you will have to switch on the type tag again and call the appropriate destructor.

Since not all types require calling a constructor and a destructor, you could take advantage of tools like std::is_pod and use some template tricks to avoid calling constructor/destructor where it is not needed, but that will add some complexity to the code, so consider this as an optimization that might take some work.


Other bits:

  • Names with double underscore are reserved for use in the Standard Library internals and for compiler extensions. Reference.

  • You should pay more mind to naming in a few places. yv or _e are pretty unclear. Your code was not very complex this time, so they didn't completely impede understanding, but don't be lazy, think of good names always!

  • It is not very usual to this-> qualify member variables in C++ classes. The usual scenario for that is when someone is trying to disambiguate a shadowed/conflicting name, which is just a way of hiding a much worse problem. If you'd like to differentiate the member variables from local ones, consider using a prefix like m_ (m_something) or a suffix _ (something_).

  • No need to expose <iostream> in your header file. Keep your headers clean by only importing what is needed.

  • This was already mentioned and it is worth repeating: Make thorough use of const whenever it makes sense. Declare methods that won't change member data as const. See What is a "const member function"?.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.