Considering the amount of pain I had trying to work out how to do this; I thought I’d write up something lengthy to explain how it works. It also helps me find if there’s anything in it that I don’t fully understand, else I wouldn’t be able to explain it.
The full Variant types header file has already been posted. This is the code I’m going off of.
Substituting Template Params
Aside from the usual header stuff, namespace and some includes, the first piece of code is
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
template<class T> struct Tag { using type=T; }; template struct Substitute : Tag {}; template struct Substitute<A,A,B> : Tag<b> {}; template using SubstituteType = typename Substitute<X,A,B>::type; template struct Substitute<X&,A,B> : Tag<SubstituteType<X,A,B>&> {}; template struct Substitute<X&&,A,B> : Tag<SubstituteType<X,A,B>&&> {}; template struct Substitute : Tag<SubstituteType<X,A,B>const> {}; template struct Substitute : Tag<SubstituteType<X,A,B>volatile> {}; template struct Substitute : Tag<SubstituteType<X,A,B>const volatile> {}; template<templateclass Z,class...Xs, class A, class B> struct Substitute<Z,A,B> : Tag<Z<SubstituteType<Xs,A,B>...>> {}; template<template<class,size_t>class Z,class X,size_t n, class A, class B> struct Substitute<Z<X,n>,A,B> : Tag<Z<SubstituteType<X,A,B>,n>> {}; template struct Substitute<R(Xs...),A,B> : Tag<SubstituteType<R,A,B>(SubstituteType<Xs,A,B>...)> {}; struct OwnType {}; </b> |
These templates allow you to swap one template type for another. It does this with these two specialized struct
s:
1 2 3 4 5 |
template<class X, class A, class B> struct Substitute : Tag<X> {}; template<class A, class B> struct Substitute<A,A,B> : Tag<B> {}; |
This lets you then do something like this in another template class/function:
1 |
SubstituteType<Type1,Type2,Type3> someVar; |
(remember that SubstituteType
is a typedef
of Substitute<X,A,B>::type
)
The type of some someVar
is going to Type1
if Type1
and Type2
are different types (invoking the first specialisation) or someVar
will be Type3
if types Type1
and Type2
are the same (by invoking the second specialisation).
The typedef
just makes it nicer on the eyes, it could have been written as:
1 |
Substitute<Type1,Type2,Type3>::type someVar; |
Probably more of a matter of taste.
The other specialisations are in order to correctly handle constness, volatileness, r-values and l-values. Finally, the last couple of specialisations allow for templatised classes.
Notice that we only care about handling the first template param, because the other two are just what we want to check and substitute with if it matches. We pass on the constness etc via the template parameter to the inherited Tag
type which is the type
member of one of the two original specialisations (the ones that actual do the comparison and swap).
All of this is needed later for when we want to allow our Variant
to contain it’s own type. I.e. a Recursive Variant.
This is done via the OwnType
struct. Basically, in order to allow the Variant to contain members of itself, you simply include OwnType
in it’s template parameter arguments. Then later, when the Variant is doing it’s thing, it can check if one of the types it hold is OwnType
, which it does through template recursion, and if it is it can replace it with a Variant using it’s own template argument list.
This is shown later.
Variant Helper
The next interesting thing is the variant helper:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 |
template<typename T0, typename... Ts> struct VariantHelper; template<typename T0, typename F, typename... Ts> struct VariantHelper<T0, F, Ts...> { inline static std::type_index InvalidType() { return std::type_index(typeid(void)); } inline static void Destroy(const std::type_index typeIndex, void* data) { if(typeIndex == std::type_index(typeid(SubstituteType<F,OwnType,T0>))) { reinterpret_cast<SubstituteType<F,OwnType,T0>*>(data)->~SubstituteType<F,OwnType,T0>(); } else if(typeIndex == InvalidType()) { } else VariantHelper<T0,Ts...>::Destroy(typeIndex, data); } inline static void Move(const std::type_index oldTypeIndex, void* oldV, void* newV) { if(oldTypeIndex == std::type_index(typeid(SubstituteType<F,OwnType,T0>))) new (newV) SubstituteType<F,OwnType,T0>(std::move(*reinterpret_cast<SubstituteType<F,OwnType,T0>*>(oldV))); else VariantHelper<T0,Ts...>::Move(oldTypeIndex,oldV,newV); } inline static void Copy(const std::type_index oldTypeIndex, const void* oldV, void* newV) { if(oldTypeIndex == std::type_index(typeid(SubstituteType<F,OwnType,T0>))) new (newV) SubstituteType<F,OwnType,T0>(*reinterpret_cast<const SubstituteType<F,OwnType,T0>*>(oldV)); else VariantHelper<T0,Ts...>::Copy(oldTypeIndex,oldV,newV); } inline static bool TypeIsValid(const std::type_index typeToCheck) { if(typeToCheck == std::type_index(typeid(SubstituteType<F,OwnType,T0>))) return true; else return VariantHelper<T0,Ts...>::TypeIsValid(typeToCheck); } inline static bool TypeIsValidRecursiveCheck(const std::type_index recursiveWrapperType) { if(recursiveWrapperType == std::type_index(typeid(SubstituteType<F,OwnType,T0>))) return true; else return VariantHelper<T0,Ts...>::TypeIsValidRecursiveCheck(recursiveWrapperType); } }; template<typename T0> struct VariantHelper<T0> { inline static void Destroy(const std::type_index typeIndex, void* data) {} inline static void Move(const std::type_index oldTypeIndex, void* oldV, void* newV) {} inline static void Copy(const std::type_index oldTypeIndex, const void* oldV, void* newV) {} inline static bool TypeIsValid(const std::type_index typeToCheck) { return false; } inline static bool TypeIsValidRecursiveCheck(const std::type_index recursiveWrapperType) { return false; } }; |
This class is used internally by the actual Variant class to handle the template recursion necessary to act on whatever type it holds. I.e., the Variant itself doesn’t know the type to cast the data to (when copying or moving) because it doesn’t know which of it’s template parameter types is currently being stored. The recursion in the helper type keeps going through each one until it finds the matching type, and at that point it then knows the type to cast to.
Ordinarily, you would only see this with two template parameters, the first one T1
and the remainder ...Ts
. That way the recursion just involves sending the remainder (...Ts
) if the type of the stored value doesn’t match T1
.
However, I added an additional template parameter, T0
, which I use to pass the type of the Variant class itself. This way the helper variant can use the substitution already discussed to replace any instance of OwnType
in the Variants parameter list, with the type of the actual Variant (passed in T0
).
You’ll see this later in the Variant, it declares the helper type as:
1 |
using HelperType = VariantHelper<Variant<Ts...>,Ts...>; |
Which means as HelperType/VariantHelper is recursing through the types in ...Ts
(the second param) it’s checking if each one is OwnType
, and if it is, it replaces it with Variant<Ts...>
(via the SubstituteType
) (what’s in T0
). Really cool stuff.
The Variant Itself
Finally, we get to the Variant class itself
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 |
template<typename... Ts> class Variant { public: Variant() : m_typeIndex(InvalidType()){} Variant(const Variant<Ts...>& old) : m_typeIndex(old.m_typeIndex) { HelperType::Copy(old.m_typeIndex, &old.m_data, &m_data); } Variant(Variant<Ts...>&& old) : m_typeIndex(old.m_typeIndex) { HelperType::Move(old.m_typeIndex, &old.m_data, &m_data); } template<typename T> Variant(const T& value) : m_typeIndex(InvalidType()) { Set<T>(value); } Variant<Ts...>& operator=(Variant<Ts...> old) { Variant<Ts...> temp(*this); m_typeIndex = old.m_typeIndex; HelperType::Copy(old.m_typeIndex,&old.m_data,&m_data); old.m_typeIndex = temp.m_typeIndex; HelperType::Copy(temp.m_typeIndex,&temp.m_data,&old.m_data); return *this; } template<typename T> bool Is() const { return (m_typeIndex == std::type_index(typeid(T))); } bool IsValid() const { return (m_typeIndex != std::type_index(typeid(InvalidType()))); } std::type_index GetTypeIndex() const { return m_typeIndex; } template<typename T> Variant<Ts...>& operator=(const T& rhs) { Set<T>(rhs); return *this; } template<typename T, typename... Args> void Set(Args&&... args) { if(!HelperType::TypeIsValid(std::type_index(typeid(T))) && !HelperType::TypeIsValidRecursiveCheck(std::type_index(typeid(RecursiveWrapper<T>)))) throw std::logic_error("Type not supported in Variant template (" + std::string(typeid(T).name()) + ")"); if(m_typeIndex != InvalidType()) { HelperType::Destroy(m_typeIndex,&m_data); m_typeIndex = InvalidType(); } if(!HelperType::TypeIsValidRecursiveCheck(std::type_index(typeid(RecursiveWrapper<T>)))) { new (&m_data) T(std::forward<Args>(args)...); m_typeIndex = std::type_index(typeid(T)); } else { new (&m_data) RecursiveWrapper<T>(std::forward<Args>(args)...); m_typeIndex = std::type_index(typeid(RecursiveWrapper<T>)); } } template<typename T> operator T() const { return Get<T>(); } template<typename T> const T& Get() const { if(m_typeIndex == std::type_index(typeid(T))) return *reinterpret_cast<const T*>(&m_data); else if(m_typeIndex == std::type_index(typeid(RecursiveWrapper<T>))) return (*reinterpret_cast<const RecursiveWrapper<T>*>(&m_data)).Get(); else throw std::bad_cast(); } template<typename T> T& Get() { if(m_typeIndex == std::type_index(typeid(T))) return *reinterpret_cast<T*>(&m_data); else if(m_typeIndex == std::type_index(typeid(RecursiveWrapper<T>))) return (*reinterpret_cast<RecursiveWrapper<T>*>(&m_data)).Get(); else throw std::bad_cast(); } ~Variant() { HelperType::Destroy(m_typeIndex,&m_data); } private: using DataType = typename std::aligned_union<1,Ts...>::type; using HelperType = VariantHelper<Variant<Ts...>,Ts...>; static inline std::type_index InvalidType() { return std::type_index(typeid(void)); } std::type_index m_typeIndex; DataType m_data; }; |
Which honestly isn’t that interesting. The DataType
is defined as the type of an std::aligned_union
, basically something that can store all these types in Ts...
. The HelperType
we already discussed.
All that’s really happening is that we rely on the Helper Type to actually do most of the heavy lifting. There’s some casting and constructors to allow you to reference the Variant type a little more naturally rather than having to explicitly call the unwieldy Get
and Set
methods.
The Get
and Set
methods are the most interesting part though. What’s unique about them is that they’re specifically looking to see if we’re currently storing a RecursiveWrapper
for one of the types; this lets us store classes that contain a Variant which also holds types of that class; and it lets us do it without having to specifically wrap instances of that class in the RecursiveWrapper
ourselves.
Recursive Wrapper
So firstly we need the RecursiveWrapper
so that a class can contain a Variant that can hold instances of that class (because pointers can point to as-yet not fully defined types).
1 2 3 4 |
class SomeType { Variant<RecursiveWrapper<SomeType>> something; }; |
You cannot write this as:
1 2 3 4 |
class SomeType { Variant<SomeType> something; }; |
Because SomeType
hasn’t been fully defined when you’re trying to declare it in the Variant template params; RecursiveWrapper
basically just holds a pointer instead.
Our Variant class is aware of the RecursiveWrapper, so now, if I had a method in SomeType
that provided a value to something, I can write:
1 2 3 4 5 |
class SomeType { Variant<RecursiveWrapper<SomeType>> something; void SetMethod(SomeType s) { something = s; } }; |
I.e., I don’t need to wrap my s
in a RecursiveWrapper
because Variant is already aware of it.