C++: Check if a type is an instantiation of a given class template
is_instance_of
type trait.
A 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.
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;
}