C++ Variant Type Walkthrough

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

These templates allow you to swap one template type for another. It does this with these two specialized structs:

This lets you then do something like this in another template class/function:

(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:

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:

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:

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

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

You cannot write this as:

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:

I.e., I don’t need to wrap my s in a RecursiveWrapper because Variant is already aware of it.

 

Leave a Reply

Your email address will not be published. Required fields are marked *