Search code examples
c++template-meta-programming

Union over variadic template parameters


Given a variadic list of types Ts..., can we create a union object that holds exactly a union of these types (and no extra data)?

Certainly this is possible with std::variant. However, the critical design piece here is that for each instance of this union, I will always know what type it holds, and so it seems wasteful to waste the space on the tag of std::variant.

To elaborate more on the use case here, I'm trying to make this work, which is a tuple that A) can be accessed by element and B) can be moved from.


Solution

  • Yes, obviously it is possible because std::variant is implementable.

    However, the core language unfortunately doesn't provide any nice syntax sugar for it.

    Essentially you need to do something like

    template<typename T, typename... Ts>
    struct variadic_union {
        union {
            T t;
            variadic_union<Ts...> ts;
        };
    };
    
    template<typename T>
    struct variadic_union<T> {
        T t;
    };
    

    and then similarly define recursive member functions to access the contents.

    Because the linear nesting may negatively affect compile times, if you have many element types, it may be better to instead split the types in each layer by half to get something like a binary tree nesting instead.

    The nesting is not a problem at runtime though. Even if e.g. a get method needs to recursively call down to other get member functions, the resulting call chain will be optimized into a single call to the base get via inlining. Access to the elements will be possible in constant time, except in a debug compile mode or if your compiler doesn't do a good job implementing inlining. This kind of inlining is essential to make C++ abstractions work. Without it a lot of C++ constructs that ought to be "zero-cost abstractions" won't be.


    There is also a much simpler solution for a variant-like type that doesn't require a union at all:

    template<typename T, typename... Ts>
    struct tagless_variant {
        alignas(Ts...) std::byte buffer[std::max({sizeof(Ts)...})];
    };
    

    Here you can then use placement-new and explicit destructor calls to implement creation/destruction of elements of any of the types. To retrieve an element you would need to return *std::launder(reinterpret_cast<U*>(buffer)) where U is the stored type.

    The problem with this solution is that it can't be made constexpr-friendly.


    In any case, both solutions will require you to make certain assumptions on the type. In particular, because the class itself doesn't know the stored type, it can't call the correct destructor automatically. So either the user will be required to explicitly reset the object or the class must only allow types with trivial destructor or the destructors are simply skipped, which can easily cause resource leaks.

    If you intent the type to be copyable or movable, then you will need to similarly restrict yourself to trivially-copyable types.