10
\$\begingroup\$

My goal is to learn a bit more about generic programming in C++. So, one of the trickiest things I've heard you can do is creating a Variant class. This took me a while and I needed to read and study a few implementations:

#ifndef VARIANT_HPP_INCLUDED
#define VARIANT_HPP_INCLUDED

#include <cstddef>
#include <typeindex>
#include <type_traits>

#include <iostream>

namespace spaceengine
{
    namespace utils
    {
        template<typename... Ts>
        struct VariantHelper;

        template<typename F, typename... Ts>
        struct VariantHelper<F, Ts...>
        {
            inline static void Destroy(const std::type_index typeIndex, void* data)
            {
                if(typeIndex == std::type_index(typeid(F)))
                    reinterpret_cast<F*>(data)->~F();
                else
                    VariantHelper<Ts...>::Destroy(typeIndex,data);
            }

            inline static void Move(const std::type_index oldTypeIndex, void* oldV, void* newV)
            {
                if(oldTypeIndex == std::type_index(typeid(F)))
                    new (newV) F(std::move(*reinterpret_cast<F*>(oldV)));
                else
                    VariantHelper<Ts...>::Move(oldTypeIndex,oldV,newV);
            }

            inline static void Copy(const std::type_index oldTypeIndex, const void* oldV, void* newV)
            {
                if(oldTypeIndex == std::type_index(typeid(F)))
                    new (newV) F(*reinterpret_cast<const F*>(oldV));
                else
                    VariantHelper<Ts...>::Copy(oldTypeIndex,oldV,newV);
            }
        };

        template<>
        struct VariantHelper<>
        {
            inline static void Destroy(const std::type_index typeIndex, void* data){}
            inline static void Move(const std::type_index oldTypeIndex, void* oldV, void* newV) {}
            inline static void Copy(const std::type_index oldTypeIndex, const void* oldV, void* newV) {}
        };

        template<typename... Ts>
        class Variant
        {
        public:
            Variant() : m_typeIndex(InvalidType()){}

            Variant(const Variant<Ts...>& old) : m_typeIndex(old.m_typeIndex)
            {
                HelperType::Copy(old.m_typeIndex, &old.m_data, &m_data);
            }

            Variant(Variant<Ts...>&& old) : m_typeIndex(old.m_typeIndex)
            {
                HelperType::Move(old.m_typeIndex, &old.m_data, &m_data);
            }

            Variant<Ts...>& operator=(Variant<Ts...> old)
            {
                m_typeIndex = old.m_typeIndex;
                Variant<Ts...> temp(*this);
                HelperType::Copy(old.m_typeIndex,&old.m_data,&m_data);
                HelperType::Copy(temp.m_typeIndex,&temp.m_data,&old.m_data);

                return *this;
            }

            template<typename T>
            bool Is() const
            {
                return (m_typeIndex == std::type_index(typeid(T)));
            }

            bool IsValid() const
            {
                return (m_typeIndex != std::type_index(typeid(InvalidType())));
            }

            std::type_index GetTypeIndex() const { return m_typeIndex; }

            template<typename T, typename... Args>
            void Set(Args&&... args)
            {
                HelperType::Destroy(m_typeIndex,&m_data);
                new (&m_data) T(std::forward<Args>(args)...);
                m_typeIndex = std::type_index(typeid(T));
            }

            template<typename T>
            const T& Get() const
            {
                if(m_typeIndex == std::type_index(typeid(T)))
                    return *reinterpret_cast<const T*>(&m_data);
                else
                    throw std::bad_cast();
            }

            template<typename T>
            T& Get()
            {
                if(m_typeIndex == std::type_index(typeid(T)))
                    return *reinterpret_cast<T*>(&m_data);
                else
                    throw std::bad_cast();
            }

            ~Variant()
            {
                HelperType::Destroy(m_typeIndex,&m_data);
            }

        private:

            using DataType = typename std::aligned_union<1,Ts...>::type;
            using HelperType = VariantHelper<Ts...>;

            static inline std::type_index InvalidType()
            {
                return std::type_index(typeid(void));
            }

            std::type_index m_typeIndex;
            DataType m_data;
        };
    }
}

#endif // VARIANT_HPP_INCLUDED

What I'm really looking for is:

  1. Is there anything that's usually required by a Variant that this class doesn't provide?
  2. Have I missed anything else that's obvious?
  3. Does my use of placement new mean that my Variant class is stack allocated, or have I misunderstood what placement new does?

One of the things that irks me is that the boost::variant class is very large; and it seems like my much smaller code does 100% of what I need. But I can't decipher boost::variant well enough to figure out what it does that mine doesn't do.

\$\endgroup\$
3
+100
\$\begingroup\$

Does my use of placement new mean that my Variant class is stack allocated, or have I misunderstood what placement new does?

It does mean that, and your implementation using the std::union to provide sufficient allocated space appears to be valid.

Almost at least, because your implementation of Variant::Set() is not checking types. So if a type is accidentally passed which wasn't covered within Ts, respectively requires more allocated space, it is going to corrupt the memory.


But I can't decipher boost::variant well enough to figure out what it does that mine doesn't do.

For instance, the boost implementation gives an never empty guarantee which your implementation does not. You are explicitly handling the case in which the Variant is in an invalid state.


In Variant::Set(), you are explicitly destroying old m_data, but you are not reverting m_typeIndex to a safe state prior to invoking the new constructor. If the new constructor would throw, it would leave m_typeIndex on the old value which is plain wrong.

Consider adding an Variant::Unset() method which handles save destruction instead and is guaranteed to set m_typeIndex explicitly to the invalid type.

This also leads to invoking the destructor on invalid states.


That recursion in the template of VariantHelper. Not going to lie - that's rather difficult to trace. A comment would have been in place, explaining what you are going to do.

\$\endgroup\$
  • \$\begingroup\$ Thanks for your review. Some questions, regarding Set, is there a way to check types at compile time? I've been scratching my head trying to see a way to do it (it's simple to add check for VariantHelper to return true, else loop). When you say it doesn't support primitive types, do you mean int, bool etc? Because it certainly does; I'm missing something here. The VarientHelper just loops through itself until the first parameter in the parameter pack is the one desired. \$\endgroup\$ – NeomerArcana May 8 '16 at 1:09
  • \$\begingroup\$ About the built-in types, my fault. I wasn't aware that calling the destructor is explicitly allowed even for built-in types, as long as it is in the scope of a template. As for the type checking, you can do something like this for minimum effort: stackoverflow.com/a/16924234/2879325 , recursing through the list, and finally wrapped in a static_assert. \$\endgroup\$ – Ext3h May 8 '16 at 10:37
  • \$\begingroup\$ What does std::union mean? \$\endgroup\$ – L. F. Apr 20 at 13:24

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.