I’ve been playing around with type meta-tagging for my Voy reactive streams library (more on that some other time) and realized how useful it is to be able to check whether a given type is an instantiation of some class template, so I decided to write a short post about it.
Imagine you are writing a generic function, and you need to check whether you are given a value or a tuple so that you can unpack the tuple before doing anything with it.
If we want to check whether a type T
is an instance of
std::tuple
or not, we can create the following
meta-function:
template <typename T>
struct is_tuple: std::false_type {};
template <typename... Args>
struct is_tuple<std::tuple<Args...>>: std::true_type {};
The meaning of this code is simple:
- By default, we return
false
for any type we are given - If the compiler is able to match the type
T
withstd::tuple<Args...>
for some list of types, then we returntrue
.
To make it easier to use, we can implement a _v
version
of the function like the meta-functions in
<type_traits>
have:
template <typename T>
constexpr bool is_tuple_v = is_tuple<T>::value;
We can now use it to implement a merged
std::invoke+std::apply
function which calls
std::apply
if the user passes in a tuple, and
std::invoke
otherwise:
template <typename F, typename T>
auto call(F&& f, T&& t)
{
if constexpr(is_tuple_v<T>) {
return std::apply(FWD(f), FWD(t));
} else {
return std::invoke(FWD(f), FWD(t));
}
}
Up one level
The previous meta-function works for tuples. What if we needed to
check whether a type is an instance of std::vector
or
std::basic_string
?
We could copy the previously defined meta-function, and replace all occurrences of “tuple” with “vector” or “basic_string”. But we know better than to do copy-paste-oriented programming.
Instead, we can increase the level of templatedness.
For STL algorithms, we use template functions instead of ordinary functions to allow us to pass in other functions as arguments. Here, we need to use template templates instead of ordinary templates.
template <template <typename...> typename Template,
typename Type>
struct is_instance_of: std::false_type {};
template <template <typename...> typename Template,
typename... Args>
struct is_instance_of<Template, Template<Args...>>: std::true_type {};
template <template <typename...> typename Template,
typename Type>
constexpr bool is_instance_of_v = is_instance_of<Template, Type>::value;
The template <template <typename...>
allows us
to pass in template names instead of template instantiations (concrete
types) to a template meta-function.
We can now check whether a specific type is an instantiation of a given template:
static_assert(is_instance_of_v<std::basic_string, std::string>);
static_assert(is_instance_of_v<std::tuple, std::tuple<int, double>>);
static_assert(!is_instance_of_v<std::tuple, std::vector<int>>);
static_assert(!is_instance_of_v<std::vector, std::tuple<int, double>>);
A similar trick is used alongside void_t
to implement
the detection
idiom which allows us to do some compile-time type introspection and
even simulate concepts.
I’ll cover the detection idiom in some of the future blog posts.