14

My aim is to write std::variant, may be not full blown, but at least with fully working constructor/destructor pair and std::get<>() function.

I tried to reserve a memory using char array. The size of it is determined by the biggest type, which is found by using find_biggest_size<>() function. The constructor uses static assert, because it performs check if the type is in the list of specified types. For now, the constructor and in place constructor works.

template <typename ... alternatives>
class variant
{
    char object[find_biggest_size<alternatives...>::value];
public:
    template <typename T>
    variant(T&& other)
    {
        static_assert(is_present<T, alternatives...>::value, "type is not in range");
        new ((T*)&object[0]) T(std::forward<T>(other));
    }

    template <typename T, typename ... ArgTypes>
    variant(in_place_t<T>, ArgTypes&& ... args)
    {
        static_assert(is_present<T, alternatives...>::value, "type is not in range");
        new ((T*)&object[0]) T(std::forward<ArgTypes>(args)...);
    }

    ~variant()
    {
        // what to do here?
    }
};

Then I've stumbled upon a problem. I don't know what destructor to execute when the object dies. On top of that, it is impossible to access the underlying object, since I can't specialize std::get<>() to get the right type.

My question is: how to store the type after the creation of the object? Is it the right approach? If not, what should I use?

EDIT:

I tried to apply the comments. The problem is that the index of the type that is currently alive can't be constexpr, thus I can't extract the needed type from type list and invoke appropriate destructor.

~variant()
{
    using T = typename extract<index, alternatives...>::type;
    (T*)&object[0]->~T();
}

EDIT:

I've made a baseline implementation. It works, but has lots of missing features. You can find it here. I would be glad to receive a review, but please first read how do I write a good answer?.

9
  • A variant keeps track of the type of the object alive in it. E.g. by an index.
    – Columbo
    Commented Sep 2, 2016 at 15:41
  • 4
    Also, std::aligned_union exists. Commented Sep 2, 2016 at 15:53
  • 1
    @OlzhasZhumabek: "isn't it what I've written just with the fancy alignment?" No, because you didn't align it to the maximum alignment of all of the types. "the problem is that I can't get it to be constexpr" You have to write it in a way that doesn't require it to be constexpr. After all, the type cannot be a constant expression, since the whole point of variant is that the type it stores is determined at runtime. Commented Sep 2, 2016 at 15:58
  • 1
    @NicolBolas, man, I've got what you said. Though I still need some pieces. I'll post the implementation when I'll collect them all. Commented Sep 2, 2016 at 16:02
  • 3
    We had a lightning talk on the tricky parts of variant in our C++ UG a while ago. Here are the slides: slideshare.net/ComicSansMS/…
    – midor
    Commented Sep 2, 2016 at 16:20

2 Answers 2

10

How I'd probably start:

#include <iostream>
#include <utility>
#include <array>

template<class...Types>
struct variant
{
    variant() {}
    ~variant()
    {
        if (type_ >= 0)
        {
            invoke_destructor(type_, reinterpret_cast<char*>(std::addressof(storage_)));
        }
    }

    template<class T> static void invoke_destructor_impl(char* object)
    {
        auto pt = reinterpret_cast<T*>(object);
        pt->~T();
    }

    static void invoke_destructor(int type, char* address)
    {
        static const std::array<void (*)(char*), sizeof...(Types)> destructors
        {
            std::addressof(invoke_destructor_impl<Types>)...
        };
        destructors[type](address);
    }

    std::aligned_union_t<0, Types...> storage_;
    int type_ = -1;

};

int main()
{
    variant<int, std::string> v;

}
8
  • 1
    FYI: std::variant is a "never empty" variant. So it doesn't default-construct to not having a value. Commented Sep 2, 2016 at 16:28
  • 1
    @NicolBolas well, "almost never empty" as I understand it, but I wanted to focus only on the destructor as that seemed to be the crux of the question. The constructors and accessors follow from there I think. Commented Sep 2, 2016 at 16:32
  • So you basically enumerated destructors? I thought about that, but couldn't find a way to do that. Commented Sep 2, 2016 at 16:36
  • @OlzhasZhumabek if you make the array static const then I think you'll find that it gets enumerated at compile time. I doubt there'll be any runtime overhead. will edit Commented Sep 2, 2016 at 16:48
  • @OlzhasZhumabek yes, in gcc 5.4 and clang 3.8 the static const array is generated at compile time. in gcc 6.2 it goes through the normal static creation check (this has to be a regression in gcc constant folding?) Commented Sep 2, 2016 at 16:56
0

First of all, you need to know which object is currently in the variant. If you want to get a type from it, that is not currently in it, you must throw an exception.

For the storage I use a union (as I do here to make it constexpr); you can't use the placement new operator as a constexpr so I think the union is the only actual way to do that (which means the only one I came up with). Mind you: you still need to explicitly call the destructor. Which yields the strange workaround I have, because a type used in a constexpr must be trivially destructible.

Now: you can implement a class similar to find_biggest_size which gives you the type from an int as a template parameter. I.e. something like that (incomplete example) :

template<int idx, typename ...Args>
struct get_type;

template<int idx, typename First, typename ...Rest>
struct get_type<idx, First, Rest...>
{
    using type = typename get_type<idx-1, Rest>::type;    
};

template<typename First, typename ...Rest>
struct get_type<0, First, Rest...>
{
    using type = First;
};
//plus specialization without Rest

And then you can implement the get function:

template<int i, typename ...Args>
auto get(variant<Args...> & v) -> typename get_type<i, Args...>::type
{ /* however you do that */ }

I hope that helps.

1
  • 3
    That doesn't explain how to take a runtime integer and figure out which constructor to call. Commented Sep 2, 2016 at 16:39

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