Last active
November 3, 2020 07:10
-
-
Save pniedzielski/0e6c48c0ac59ab16ed29 to your computer and use it in GitHub Desktop.
Type-safe generation of variant<Ts...> in C++
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Say we want something that generates a T when it is called with | |
// operator(): | |
template <typename G, typename T> | |
concept bool Generator = requires (G g) { | |
{ g() } -> T | |
}; | |
// For instance, this is a Generator of int | |
int gimmeRandomInt() { return rand(); } | |
// As is this: | |
int gimmeZero() { return 0; } | |
// Or more interestingly, objects of this type template is a generator | |
// of an arbitrary T: | |
template <typename T> | |
struct generate { | |
T operator() (); | |
}; | |
// For example, | |
template <> | |
struct generate<int> { | |
auto operator() () { return rand(); } | |
}; | |
// which can be called as generate<int>{}() (construct new | |
// generate<int> with braces, then call it with parens). | |
// Interesting case is for variant on arbitrary Ts... though. | |
template <typename... Ts> | |
struct generate<variant<Ts...>> { | |
variant<Ts...> operator() (); | |
}; | |
// We know how to easily generate a variant given a type to put in it: | |
/* | |
template <typename T, typename... Ts> | |
struct generate_variant_from_T { | |
variant<Ts...> operator() () { | |
auto generator = generate<T>{}; | |
return variant<Ts...>{ generator() }; | |
} | |
}; | |
*/ | |
// However, this will give us a different type for every T. Because | |
// we need to select from a set of generators at runtime (based on a | |
// randomly generated value), we need them to be of the same runtime | |
// type. Thus, we need to erase the compile time type, using runtime | |
// polymorphism. | |
namespace details { | |
// Abstract Base Class for all variant generators. | |
template <typename... Ts> | |
struct generate_variant { | |
virtual variant<Ts...> operator() () = 0; | |
}; | |
// Make a template for which all instantiations derive from | |
// generate_variant<Ts...> | |
template <typename T, typename... Ts> | |
struct generate_variant_from_T : public generate_variant<Ts...> { | |
virtual variant<Ts...> operator() () override { | |
auto generator = generate<T>{}; | |
return variant<Ts...>{ generator() }; | |
} | |
}; | |
} | |
// If we make pointers to each of these on the heap, the runtime type | |
// is generate_variant<Ts...>, but the compile-time type is | |
// generate_variant_from_T<T, Ts...>. This means we can do dynamic | |
// dispatch and generate a variant with the given type T in a typesafe | |
// way at runtime, for an arbitrary T or Ts... Our actual generator | |
// looks like this: | |
template <typename Ts...> | |
struct generate<variant<Ts...>> { | |
auto operator() () { | |
using namespace details; | |
// First, we make all generators. We create a generate_variant | |
// pointer for each type T in Ts... We could use an array | |
// instead, because the size is known at compile time (one for | |
// each Ts...). | |
auto generators = vector<unique_ptr<generate_variant<Ts...>>>{ | |
make_unique( generate_variant_from_T<Ts, Ts...>{} )... | |
}; | |
// Now, we select a random index into the vector. | |
auto random_index = get_rand_between(0, size(generators)); | |
// Finally, we select that generator, and use it to generate a | |
// variant. | |
auto& generator = generators[random_index]; | |
return generator(); | |
} | |
}; | |
// Now, for any variant<Ts...>, we can generate a value of it over its | |
// entire range. If get_rand_between has some more information about | |
// each type (we make a metafunction that says how many possible | |
// values there are for each T in Ts...), we can make this generate | |
// uniformly across the entire space of values of variant<Ts...>. | |
// If we know the layout of our types (implementation-defined, but the | |
// classes are empty and the vtable layouts will probably look the | |
// same), we can even avoid the need for the heap memory all-together. | |
// Doing so is unsafe, but, along with devirtualization, would make | |
// this trivially inlinable and very fast. |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment