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 with std::tuple<Args...> for some list of types, then we return true.

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.