4
\$\begingroup\$

I was amazed by std::variant and got inspired to write my own. For now, it supports constructor+destructor pair, assignment operator and get<>() function to retrieve the value. The object doesn't allocate any dynamic memory. All of the supported functions are type safe.

I wanted to use static_assert(), but I found that it triggers only during build on MSVC. It could provide clearer compilation errors, but std::enable_if_t<> provides error messages right when programmer is typing code, so I've chosen the latter.

T() is used to zero initialize POD types.

My concern about the code is very heavy use of template meta programming. Most of the standard library classes are lightweight. Other suggestions are welcome as well.

Here is the variant:

#ifndef CUSTOM_VARIANT_H
#define CUSTOM_VARIANT_H

#include <type_traits>
#include <utility>
#include <array>

namespace impl
{
    template <std::size_t ind, typename T, typename ... types>
    struct is_present
    {};

    template <std::size_t ind, typename T, typename first, typename ... rest>
    struct is_present<ind, T, first, rest...>
    {
        static constexpr bool value = (std::is_same<T, first>::value) ? true : is_present<ind + 1, T, rest...>::value;
        static constexpr std::size_t index = (std::is_same<T, first>::value) ? ind : is_present<ind + 1, T, rest...>::index;
    };

    template <std::size_t ind, typename T, typename first>
    struct is_present<ind, T, first>
    {
        static constexpr bool value = std::is_same<T, first>::value;
        static constexpr std::size_t index = ind;
    };

    template <typename ... types>
    struct pick_first {};

    template <typename first, typename ... rest>
    struct pick_first<first, rest...>
    {
        using type = first;
    };

    template <typename ... types>
    using pick_first_t = typename pick_first<types...>::type;
}

template <typename T, typename ... types>
using is_present = impl::is_present<0, T, types...>;

template <std::size_t ind, typename ... Types>
struct extract
{};

template <std::size_t ind, typename first, typename ... rest>
struct extract<ind, first, rest...>
{
    using type = typename extract<ind - 1, rest...>::type;
};

template <typename T, typename ... rest>
struct extract<0, T, rest...>
{
    using type = T;
};

template <typename T>
struct in_place_t {};

struct bad_variant_access {};

template <typename ... alternatives>
class variant
{
    using storage_type = std::aligned_union_t<0, alternatives...>;
    storage_type storage;
    std::size_t typeindex;
public:
    variant()
    {
        using T = typename impl::pick_first<alternatives...>::type;
        new ((T*)&storage) T();
        typeindex = 0;
    }

    template <typename T>
    variant(T&& value)
    {
        using checked = typename std::enable_if<is_present<T, alternatives...>::value, T>::type;
        new ((checked*)&storage) T(std::forward<T>(value));
        typeindex = is_present<T, alternatives ...>::index;
    }

    variant(variant&& other)
    {
        invoke_move_constructor(other.typeindex, &other.storage, &storage);
        typeindex = other.typeindex;
    }

    variant(const variant& other)
    {
        invoke_copy_constructor(other.typeindex, &other.storage, &storage);
        typeindex = other.typeindex;
    }

    variant(variant& other):variant(const_cast<const variant&>(other))
    {

    }

    template <typename T, typename ... ArgTypes>
    variant(in_place_t<T>, ArgTypes&& ... args)
    {
        using checked = typename std::enable_if<is_present<T, alternatives...>::value, T>::type;
        new ((checked*)&storage) T(std::forward<T>(args)...);
        typeindex = is_present<T, alternatives ...>::index;
    }

    template <std::size_t ind, typename ... alts>
    friend typename extract<ind, alts...>::type& get(variant<alts...>& v);

    template <typename T>
    variant& operator=(const T& value)
    {
        invoke_destructor(typeindex, &storage);
        using checked = std::enable_if<is_present<T, alternatives...>::value, T>::type;
        new ((checked*)&storage) T(value);
        typeindex = is_present<T, alternatives...>::index;
        return *this;
    }

    variant& operator=(variant&& other)
    {
        invoke_move_constructor(other.typeindex, &other.storage, &storage);
        typeindex = other.typeindex;
    }

    variant& operator=(const variant& other)
    {
        invoke_copy_constructor(other.typeindex, *other.storage, &storage);
        typeindex = other.typeindex;
    }

    ~variant()
    {
        invoke_destructor(typeindex, &storage);
    }
private:
    static void invoke_destructor(std::size_t type, storage_type* store)
    {
        static const std::array<void(*)(storage_type*), sizeof...(alternatives)> destructors
        {
            std::addressof(invoke_destructor_impl<alternatives>)...
        };
        destructors[type](store);
    }

    template <typename T>
    static void invoke_destructor_impl(storage_type* store)
    {
        auto pt = reinterpret_cast<T*>(store);
        pt->~T();
    }

    static void invoke_copy_constructor(std::size_t type, const storage_type* from, storage_type* to)
    {
        static const std::array<void(*)(const storage_type*, storage_type*), sizeof...(alternatives)> copy_constructors
        {
            std::addressof(invoke_copy_constructor_impl<alternatives>)...
        };

        copy_constructors[type](from, to);
    }

    template <typename T>
    static void invoke_copy_constructor_impl(const storage_type* from, storage_type* to)
    {
        T* dest = reinterpret_cast<T*>(to);
        const T* source = reinterpret_cast<const T*>(from);
        ::new(dest) T(*source);
    }

    static void invoke_move_constructor(std::size_t type, storage_type* from, storage_type* to)
    {
        static const std::array<void(*)(storage_type*, storage_type*), sizeof...(alternatives)> move_constructors
        {
            std::addressof(invoke_move_constructor_impl<alternatives>)...
        };

        move_constructors[type](from, to);
    }

    template <typename T>
    static void invoke_move_constructor_impl(storage_type* from, storage_type* to)
    {
        auto dest = reinterpret_cast<T*>(to);
        auto source = reinterpret_cast<T*>(from);
        ::new(dest) T(std::move(*source));
    }



};

template <std::size_t ind, typename ... alts>
typename extract<ind, alts...>::type& get(variant<alts...>& v)
{
    using T = typename extract<ind, alts...>::type;
    if (v.typeindex != ind)
    {
        throw bad_variant_access{};
    }

    return *((T*)(&v.storage));
}

template <typename T>
class variant<T>
{
    T value;
public:
    variant():value()
    {}

    variant(const variant& other):
        value(other.value)
    {}

    variant(variant&& other) :
        value(other.value)
    {

    }

    template <typename ... ArgTypes>
    variant(in_place_t<T>, ArgTypes&& ... args):
        value(std::forward<ArgTypes>(args)...)
    {}

    template <std::size_t idx, typename T>
    friend T& get(variant<T>& v);
};

template <std::size_t idx, typename T>
T& get(variant<T>& v)
{
    if (idx != 0)
    {
        throw bad_variant_access{};
    }

    return v.value;
}

#endif

Here are some tests:

#include "variant.h"
#include <iostream>

struct mytype
{
    mytype() 
    {
        std::cout << "def constructor is invoked\n";
        x = 0;
        y = '0';
    }

    mytype(const mytype& other)
    {
        std::cout << "copy constructor is invoked\n";
        x = 0;
        y = '0';
    }

    mytype(mytype&& other)
    {
        std::cout << "move constructor is invoked\n";
        x = 0;
        y = '0';
    }

    int x;
    char y;

    ~mytype()
    {
        std::cout << "destructor is invoked\n";
    }
};

int main()
{   
    variant<mytype, int> first; //def ctor
    std::cout << get<0>(first).x << ' ' << get<0>(first).y << '\n';//0 0

    first = mytype{}; //def ctor, destructor, copy ctor, destructor
    variant<mytype, int> second(first); //copy ctor
    variant<mytype, int> third(std::move(first)); //move ctor

    first = 2; //destructor
    std::cout << get<1>(first) << '\n';//2
    //2 destructors
}
\$\endgroup\$
  • \$\begingroup\$ Previous post contained bugs (quite a lot of them). This one is working. \$\endgroup\$ – Incomputable Sep 10 '16 at 9:41
  • \$\begingroup\$ std::array is redundant. Why not plain old static (constexpr!) C-array? I think placement new and reinterpret_cast is not the way towards constexpr. What if one of the alternative types provided & and const && c-tors and assignment operators? Trivially copy/move-constructible,-assignable, destructible (except default-constructible, for sure) cases may also make sense. You must dispatch them. See here and here for better insights. \$\endgroup\$ – Orient Sep 13 '16 at 19:26
  • \$\begingroup\$ @Orient, this is baseline implementation, but I will keep upgrading it when I will have time. Thanks \$\endgroup\$ – Incomputable Sep 13 '16 at 20:25

Your Answer

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

Browse other questions tagged or ask your own question.