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.
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.