C++ programming language logo, white text 'C++' in shaded blue hexagon
blog

C++: Check if a type is an instantiation of a given class template

How to implement an is_instance_of type trait.
6 min read /

The previous post mulled over some awkwardness in forwarding references when it comes to function overload resolution. It proposed a design pattern of merging all function overloads into one, then using if constexpr within the one function body to check for different argument types and provide an implementation for each. The workhorse for checking argument types was std::is_same_v to check for exact matches between types, combined with std::decay_t to relax const and ref qualifications.

Here we go a little further. We can use any type traits in the implementation of such a function. We needn’t be restricted to std::is_same_v and exact comparisons between types, and can use various type traits of the C++ Standard Library. But there are other checks we may wish to make that are not supported in the standard library (as of C++23). One is whether a given type is an instantiation of a given type template, e.g. whether some type is an instantiation of std::vector, or std::list, or whatever. We show how to do that here, by implementing our own is_instance_of_v type trait.

  1. Implementation
  2. Demonstration
  3. Summary

Implementation

We would like to implement a type trait is_instance_of_v<T,U> that, for a given type T and type template U, gives true if T is an instantiation of U, and false otherwise. The following illustrates the desired behavior:

is_instance_of_v<std::vector<int>,std::vector>;  // true
is_instance_of_v<std::vector<int>,std::list>;  // false

For templates with one template parameter, we could implement is_instance_of_v as follows:

template<class T, template<class> class U>
inline constexpr bool is_instance_of_v = std::false_type{};

template<template<class> class U, class V>
inline constexpr bool is_instance_of_v<U<V>,U> = std::true_type{};

This features the curious (rare?) use of a template template parameter (yes!): a template parameter that accepts a template. This is the template<class> class U part. The declaration of is_instance_of_v (the first statement) establishes that the trait is false in the general case, then the specialization of is_instance_of_v (the second statement) establishes that it is true in the special case that the type is indeed an instantiation of the type template.

That is for one template parameter. It is readily extended to more than one by using a parameter pack:

template<class T, template<class...> class U>
inline constexpr bool is_instance_of_v = std::false_type{};

template<template<class...> class U, class... Vs>
inline constexpr bool is_instance_of_v<U<Vs...>,U> = std::true_type{};

The ... is the parameter pack. This latter version is the preferred implementation, replacing the former version.

Demonstration

Motivated by the previous post on overloading functions with forwarding references, we can use this in place of std::is_same_v where we wish to check that the type is an instantiation of a given template, not just an exact match against some other type. The caveats around const and ref qualifiers still apply, however, and the implementation of is_instance_of_v above does not consider const or ref-qualified types to be instantiations of a template. Hence the following:

is_instance_of_v<std::vector<int>,std::vector>;  // true
is_instance_of_v<std::vector<int>&,std::vector>;  // false
is_instance_of_v<std::vector<int>&&,std::vector>;  // false
is_instance_of_v<const std::vector<int>,std::vector>;  // false
is_instance_of_v<const std::vector<int>&,std::vector>;  // false

We can use std::decay_t around the type (as suggested in the previous post) to remove const and ref qualifiers:

is_instance_of_v<std::decay_t<std::vector<int>>,std::vector>;  // true
is_instance_of_v<std::decay_t<std::vector<int>&>,std::vector>;  // true
is_instance_of_v<std::decay_t<std::vector<int>&&>,std::vector>;  // true
is_instance_of_v<std::decay_t<const std::vector<int>>,std::vector>;  // true
is_instance_of_v<std::decay_t<const std::vector<int>&>,std::vector>;  // true

Or decltype(...) works too:

std::vector<int> x;
is_instance_of_v<decltype(x),std::vector>;  // true

That’s because while x has lvalue reference type std::vector<int>&, its decltype is still just std::vector<int>.

Summary

This post introduced a type trait is_instance_of_v<T,U> to check if some type T is an instantiation of some type template U. It can be used for various purposes, but might be particularly useful in the context of implementing function overloads with forwarding reference parameters as suggested by the previous post.

Here’s a complete source file for further play:

#include <type_traits>
#include <iostream>
#include <vector>
#include <list>

template<class T, template<class...> class U>
inline constexpr bool is_instance_of_v = std::false_type{};

template<template<class...> class U, class... Vs>
inline constexpr bool is_instance_of_v<U<Vs...>,U> = std::true_type{};

int main() {
  std::cerr << is_instance_of_v<std::vector<int>,std::vector> << ' ';  // true
  std::cerr << is_instance_of_v<std::vector<int>,std::list> << std::endl;  // false

  std::cerr << is_instance_of_v<std::vector<int>,std::vector> << ' ';  // true
  std::cerr << is_instance_of_v<std::vector<int>&,std::vector> << ' ';  // false
  std::cerr << is_instance_of_v<std::vector<int>&&,std::vector> << ' ';  // false
  std::cerr << is_instance_of_v<const std::vector<int>,std::vector> << ' ';  // false
  std::cerr << is_instance_of_v<const std::vector<int>&,std::vector> << std::endl;  // false

  std::cerr << is_instance_of_v<std::decay_t<std::vector<int>>,std::vector> << ' ';  // true
  std::cerr << is_instance_of_v<std::decay_t<std::vector<int>&>,std::vector> << ' ';  // true
  std::cerr << is_instance_of_v<std::decay_t<std::vector<int>&&>,std::vector> << ' ';  // true
  std::cerr << is_instance_of_v<std::decay_t<const std::vector<int>>,std::vector> << ' ';  // true
  std::cerr << is_instance_of_v<std::decay_t<const std::vector<int>&>,std::vector> << std::endl;  // true

  std::vector<int> x;
  std::cerr << is_instance_of_v<decltype(x),std::vector> << ' ';  // true
  return 0;
}