New variant implementation - #5130
Conversation
|
I think this is how one implements recursive variants with the C++ standard library. It's not a widely used design pattern, and the Boost library relies on a radically different design, so I'll give an overview of the PR implementation here. The traditional variant uses a tagged union data structure, where the union stores any object whose type belongs to the variant type list, and the tag indicates the type of the currently held object. Here is how the containers are implemented in the STL and Boost, in pseudo-code: // factory to dynamically construct a C++ union type that can hold
// any value whose type appears in the union template parameters
template<class First, class... Rest>
union variadic_union {
variadic_union(First &&value, Rest&&... rest)
: m_first(std::forward<First>(value)),
m_rest(std::forward<Rest>(rest)...) {}
First m_first;
variadic_union<Rest...> m_rest;
};
template <class... Ts>
struct std::variant {
union variadic_union<Ts...> data;
uint8_t type_index;
};
template <class... Ts>
struct boost::variant {
uint32_t type_index;
union variadic_union<Ts...> data;
};The variant type list The Boost library implements recursive variants by replacing the variant type by a pointer to the variant type, i.e. The STL doesn't provide this design pattern, which means the variant can only appear in its own type list in the form of a pointer. Which is fine with ESPResSo, since we only need the variant inside STL containers like vectors and maps, both of which hold a pointer to a data structure in heap memory. The PR implementation looks like this in pseudo-code: template <class... Ts>
struct recursive_variant
: public std::variant<Ts...,
std::vector<recursive_variant<Ts...>>,
std::unordered_map<int, recursive_variant<Ts...>>,
std::unordered_map<std::string, recursive_variant<Ts...>>> {
using BaseClass = std::variant<Ts..., ___> // here repeat the full signature of the base class
using BaseClass::BaseClass;
private:
friend class boost::serialization::access;
template <typename Archive>
void serialize(Archive &ar, unsigned const) {
BaseClass &self = *this;
ar & self;
}
};One pain point with this solution, is that like many CRTPs, one has to spell out the base class twice: once in the inheritance list, and once in a typedef so it can be referred to in the constructor and in the self. To avoid code duplication, one can declare the base class as a templated typedef outside the class, and forward-declare the CRTP class to be able to use it in the templated typedef 1. This works for simple cases, but when the forward-declared type is part of a STL template instance, type information can be lost. This is unfortunately the case with the STL of GCC 11, where the Footnotes
|
|
The replacement of the non-recursive variants in the core is probably uncontroversial. However, when it comes to the recursive_variant in the script interface, we need to discuss whether replacing the relatively clear boost::make_recursive_variant with very advanced custom C++ that few people will be able to maintain really improves our position in terms of sutainability. |
|
I'm open to this discussion. The old implementation, while more readable, comes with its one caveats: one has to remember that every time a new type is introduced in |
|
For completeness, here is a more concise solution that uses reflections: template <class Base> struct ReflectionGetParent : public Base {
using Base::Base;
using first_parent = Base;
};
template <class... Ts>
struct recursive_variant
: public ReflectionGetParent<std::variant<
Ts..., std::vector<recursive_variant<Ts...>>,
std::unordered_map<int, recursive_variant<Ts...>>,
std::unordered_map<std::string, recursive_variant<Ts...>>>> {
using ParentClass = typename recursive_variant<Ts...>::first_parent;
using BaseClass = ReflectionGetParent<ParentClass>;
using BaseClass::BaseClass;
private:
friend class boost::serialization::access;
template <typename Archive>
void serialize(Archive &ar, unsigned const /*version*/) {
ParentClass &self = *this;
ar & self;
}
};Works with libstdc++ but not with libc++, which complains about |
|
Doxygen in CI doesn't behave like on our workstations, even though the same version is used. This is getting annoying. |
Partial fix for #5119
Description of changes:
std::varianteverywhere