Determining if a template specialization exists

One C++17 problem I come across every now and then is to determine whether a certain class or function template specialization exists - say, for example, whether std::swap<SomeType> or std::hash<SomeType> can be used. I like to have solutions for these kind of problems in a template-toolbox, usually just a header file living somewhere in my project. In this article I try to build solutions that are as general as possible to be part of such a toolbox.

Note that it is indeed not entirely clear what it means that “a specialization exists”. Even though this might seem unintuitive, I’ll postpone that discussion to the last section, and will for the start continue with the intuitive sense that “specialization exists” means “I can use it at this place in the code”.

Where I mention the standard, I refer to the C++17 standard, and I usually use GCC 12 and Clang 15 as compilers. See my note on MSVC at the end for why I’m not using it in this article.


Update, 3.1.2023: /u/gracicot commented on reddit about some limitations of the below solution. This revolves around testing for the same specialization multiple times, once before the specialization has been declared, once after. This is indeed not possible (and forbidden by the C++ standard). I have added a section with the respective warning. For now, let’s go with this handwavy rule: If you want to test in multiple places whether a template Tmpl is specialized for type T, the answer must be the same in all these places.

Update, 7.1.2023: An attentive reader spotted that at the end of the section with the generic solution for class templates, I write that I now drop the assumption that every class always has a default constructor - but then still rely on that default constructor. I forgot to put in a std::declval there. Thanks for the note!

Update, 15.2.2023: This article has been published in ACCU’s Overload issue 173.


Testing for a specific function template specialization

First, the easiest part: Testing for one specific function template specialization. I’ll use std::swap as an example here, though in C++17 you should of course use std::is_swappable to test for the existence of std::swap<T>.

Without much ado, here’s my proposed solution:

 1struct HasStdSwap {
 2private:
 3  template <class T, class Dummy = decltype(std::swap<T>(std::declval<T &>(),
 4                                                         std::declval<T &>()))>
 5  static constexpr bool exists(int) {
 6    return true;
 7  }
 8
 9  template <class T> static constexpr bool exists(char) { return false; }
10
11public:
12  template <class T> static constexpr bool check() { return exists<T>(42); }
13};

Let’s unpack this: The two exists overloads are doing the heavy lifting here. The goal is to have the preferred overload when called with the argument 42 (i.e., the overload taking int) return true if and only if std::swap<T> is available. To achieve this, we must only make sure that this overload is not available if std::swap<T> does not exist, which we do by SFINAE-ing it away if the expression decltype(std::swap<T>(std::declval<T&>(), std::declval<T&>())) is malformed.

You can play with this here at Compiler Explorer. Note that we need to use std::declval<T&>() instead of the more intuitive std::declval<T>() because the result type of std::declval<T>() is T&&, and std::swap can of course not take rvalue references.

Testing for a specific class template specialization

Now that we have a solution to test for a specific function template specialization, let’s transfer this to class templates. We’ll use std::hash as an example here.

To transform the above solution, we only need to figure out what to use as default-argument type for Dummy, i.e., something that is well-formed exactly in the cases where we want the result to be true. We can’t just use Dummy = std::hash<T>, because std::hash<T> is a properly declared type for all types T! What we actually want to check is whether std::hash<T> has been defined and not just declared. If a type has only been declared (and not defined), it is an incomplete type. Thus we should use something that does work for all complete types, but not for incomplete types.

In the case of std::hash, we can assume that every definition of std::hash must have a default constructor (as mandated by the standard for std::hash), so we can do this:

 1struct HasStdHash {
 2private:
 3  template <class T, class Dummy = decltype(std::hash<T>{})>
 4  static constexpr bool exists(int) {
 5    return true;
 6  }
 7
 8  template <class T>
 9  static constexpr bool exists(char) {
10    return false;
11  }
12
13public:
14  template <class T>
15  static constexpr bool check() {
16    return exists<T>(42);
17  }
18};

This works nicely as you can see here at Compiler Explorer. This is how you can use it:

1std::cout << "Does std::string have std::hash? " << HasStdHash::check<std::string>();

A generic test for class templates

If I want to put this into my template toolbox, I can’t have a implementation that’s specific for std::hash (and one for std::less, one for std::equal_to, …). Instead I want a more general form that works for all class templates, or at least those class templates that only take type template parameters.

To do this, I want to pass the class template to be tested as a template template parameter. Adapting our solution from above, this is what we would end up with:

 1template <template <class... InnerArgs> class Tmpl>
 2struct IsSpecialized {
 3private:
 4  template <class... Args, class dummy = decltype(Tmpl<Args...>{})>
 5  static constexpr bool exists(int) {
 6    return true;
 7  }
 8  template <class... Args>
 9  static constexpr bool exists(char) {
10    return false;
11  }
12
13public:
14  template <class... Args>
15  static constexpr bool check() {
16    return exists<Args...>(42);
17  }
18};

This does still work for std::hash, as you can see here at Compiler Explorer, when being used like this:

1std::cout << "Does std::string have std::hash? " << IsSpecialized<std::hash>::check<std::string>();

However, by using Tmpl<Args...>{}, we assume that the class (i.e., the specialization we are interested in) has a default constructor, which may not be the case. We need something else that always works for any complete class, and never for an incomplete class.

If we want to stay with a type, we can use something unintuitive: the type of an explicit call of the destructor. While the destructor itself has no return type (as it does not return anything), the standard states in [expr.call]:

If the postfix-expression designates a destructor, the type of the function call expression is void; […]

So this will work regardless of how the template class is defined1 (changes highlighted):

 1template <template <class... InnerArgs> class Tmpl>
 2struct IsSpecialized {
 3private:
 4  template <class... Args,
 5            class dummy = decltype(std::declval<Tmpl<Args...>>().~Tmpl<Args...>())>
 6  static constexpr bool exists(int) {
 7    return true;
 8  }
 9  template <class... Args>
10  static constexpr bool exists(char) {
11    return false;
12  }
13
14public:
15  template <class... Args>
16  static constexpr bool check() {
17    return exists<Args...>(42);
18  }
19};

Note that we use std::declval to get a reference to Tmpl<Args...> without having to rely on its default constructor. Again you can see this at work at Compiler Explorer.

Problem: Specializations that sometimes exist and sometimes don’t

The question of whether SomeTemplate<SomeType> is a complete type (a.k.a. “the specialization exists”) depends on whether the respective definition has been seen or not. Thus, it can differ between translation units, but also within the same translation unit. Consider this case:

1template<class T> struct SomeStruct;
2
3bool test1 = IsSpecialized<SomeStruct>::check<std::string>();
4
5template<> struct SomeStruct<std::string> {};
6
7bool test2 = IsSpecialized<SomeStruct>::check<std::string>();

What should happen here? What values would we want for test1 and test2? Intuitively, we would want test1 to be false, and test2 to be true. If we try to square this with the IsSpecialized template from above, something weird happens: The same template, IsSpecialized<SomeStruct>::check<std::string>(), is instantiated with the same template arguments but should emit a different behavior. Something cannot be right here. If you imagine both tests (once with the desired result true, once with desired result false) to be spread across different translation units, this has the strong smell of an ODR-violation.

If we try this at Compiler Explorer, we indeed see that this does not work. So, what’s going on here?

The program is actually ill-formed, and there’s nothing we can do to change that. The standard states in temp.expl.spec/6:

If a template […] is explicitly specialized then that specialization shall be declared before the first use of that specialization that would cause an implicit instantiation to take place, in every translation unit in which such a use occurs; no diagnostic is required. […]

Of course the test for the availability of the specialization would “cause an implicit instantiation” (which fails and causes SFINAE to kick in).2 Thus it is always ill-formed to have two tests for the presence of a specialization if one of them “should” succeed and one “should” fail.

In fact, the standard contains a paragraph, temp.expl.spec/7, that does not define anything (at least if I read it correctly), but only issues a warning that ’there be dragons’ if one has explicit specializations sometimes visible, sometimes invisible. I’ve not known the standard to be especially poetic, this seems to be the exception:

The placement of explicit specialization declarations […] can affect whether a program is well-formed according to the relative positioning of the explicit specialization declarations and their points of instantiation in the translation unit as specified above and below. When writing a specialization, be careful about its location; or to make it compile will be such a trial as to kindle its self-immolation.

Thus, as a rule of thumb (not just for testing whether a specialization exists): If you use Tmpl<T> at multiple places in your program, you must make sure that any explicit specialization for Tmpl<T> is visible at all those places.

A generic test for function templates

The move from testing whether one particular class template was specialized for a type T to having a test for arbitrary class templates was pretty easy. Unfortunately it is a lot harder to replicate the same for function templates. This is mainly because we cannot pass around function templates as we can pass class templates as template template parameters.

If we want to have a template similar to IsSpecialized from above (let’s call it FunctionSpecExists), we need a way of encapsulating a function template so that we can pass it to our new FunctionSpecExists. On the other hand, we want to keep this “wrapper” as small as possible, because we will need it at every call site. Thus, building a struct or class is not the way to go.

C++14 generic lambdas provide a neat way of encapsulating a function template. Remember that a lambda expression is of (an unnamed) class type. Thus, we can pass them around as template parameter, like any other type.

Encapsulating the function template we are interested in (std::swap, again) in a generic lambda could look like this:

1auto l = [](auto &lhs, auto &rhs) { return std::swap(lhs, rhs); };

Now that we have something that is callable if and only if std::swap<decltype(lhs)> is available. When I write “is callable if”, this directly hints at what we can use to implement our FunctionSpecExists struct - “is callable” sounds a lot like std::is_invocable, right?

So, to test whether SomeType can be swapped via std::swap, can we just do this?

1auto l = [](auto &lhs, auto &rhs) { return std::swap(lhs, rhs); };
2bool has_swap = std::is_invocable_v<decltype(l), SomeType &, SomeType &>;

Unfortunately, no. Assuming that SomeType is not swappable, we are getting no matching call to std::swap errors. The problem here is that std::is_invocable must rely on SFINAE to remove the infeasible std::swap implementations (which in this case are all implementations). However, SFINAE only works in the elusive “immediate context” as per [temp.deduct]/8. The unnamed class that the compiler internally creates for the generic lambda looks (simplified) something like this:

1struct Unnamed {
2  template <class T1, class T2>
3  auto operator()(T1 &lhs, T2 &rhs) {
4    return std::swap(lhs, rhs);
5  }
6};

Here it becomes obvious that plugging in SomeType for T1 and T2 does not lead to a deduction failure in the “immediate context” of the function, but actually just makes the body of the operator() function ill-formed. We need the problem (no matching std::swap) to kick in in one of the places for which [temp.deduct] says that types are substituted during template deduction. Quoting from [temp.deduct]/7:

The substitution occurs in all types and expressions that are used in the function type and in template parameter declarations.

One thing that is part of the function type is a trailing return type, so we can use that. Let’s rewrite our lambda to:

1auto betterL = [](auto &lhs, auto &rhs) -> decltype(std::swap(lhs, rhs)) {
2  return std::swap(lhs, rhs);
3};

Now we have a case where, if you were to substitute the non-swappable SomeType for the auto types, there is an error in the types involved in the function type. And indeed, this actually works, as you can see here on Compiler Explorer:

1auto betterL = [](auto &lhs, auto &rhs) -> decltype(std::swap(lhs, rhs)) {
2  return std::swap(lhs, rhs);
3};
4constexpr bool sometype_has_swap =
5    std::is_invocable_v<decltype(betterL), SomeType &, SomeType &>;

I don’t think that you can further encapsulate this into some utility templates to make the calls more compact, so that’s just what I will use from now on.

What do I mean by “a specialization exists”

I wrote at the beginning that it’s not entirely clear what “a specialization exists” should even mean. It is of course not possible - neither for class templates, nor for function templates - to check at compile time whether a certain specialization exists somewhere, which may be in a different translation unit. I wrote the previous sections with the aim of testing whether the class template (resp. function template) can be “used” with the given arguments at the point where the test happens.

For class templates, I say a “specialization exists” if, for a given set of template arguments, the resulting type is not just declared, but also defined (i.e., it is a complete type). As an example:

 1template<class T>
 2struct SomeStruct;
 3
 4template<>
 5struct SomeStruct<int> {};
 6
 7// (Point A) Which specializations "exist" at this point?
 8
 9template<>
10struct SomeStruct<std::string> {};

In this code, at the marked line, only the specialization for the type int “exists”.

For function templates, it’s actually a bit more complicated, since C++ has no concept of “incomplete functions” analogous to “incomplete types”. Here, I say that a specialization “exists” if the respective overload has been declared. Take this example:

1template<class T>
2void doFoo(T t);
3
4template<class T, class Dummy=std::enable_if_t<std::is_integral_v<T>, bool> = true>
5void doBar(T t);
6template<class T, class Dummy=std::is_same_v<T, std::string>, bool> = true>
7void doBar(T t) {};
8
9// (Point B) Which specializations "exist" at this point?

At the marked, line:

  • For any type T, the specialization doFoo<T> “exists”, because the respective overload has been declared in lines one and two.
  • The two specializations doBar<std::string> and doBar<T> for any integral type T “exist”. Note that this is indenpendent of whether the function has been defined (like doBar<std::string>) or merely declared.
  • For all non-integral, non-std::string types T, the specialization doBar<T> does “not exist”.

This of course means that our “test for an existing specialization” for functions is more of a “test for an existing overload”, and can in fact be used to achieve this.

A note on MSVC and std::hash

In all my examples, I used GCC and Clang as compilers. This is because my examples for std::hash do not work with MSVC, at least if you enable C++17 (it works in C++14 mode). That is because of this (simplified) std::hash implementation in MSVC’s STL implementation:

 1template <class _Kty, bool _Enabled>
 2struct _Conditionally_enabled_hash { // conditionally enabled hash base
 3  size_t operator()(const _Kty &_Keyval) const
 4	{
 5    return hash<_Kty>::_Do_hash(_Keyval);
 6  }
 7};
 8
 9template <class _Kty>
10struct _Conditionally_enabled_hash<_Kty,
11                                   false> { // conditionally disabled hash base
12  _Conditionally_enabled_hash() = delete;
13	// *no* operator()!
14};
15
16template <class _Kty>
17struct hash
18    : _Conditionally_enabled_hash<_Kty, should_be_enabled_v<_Kty>>
19{
20  // *no* operator()!
21};

This implementation is supposed to handle all integral, enumeration and pointer types (which is what should_be_enabled_v tests for), but the point is: For all other types, this gives you a defined, and thus complete, class - which does not have an operator(). I’m not sure why the designers built this this way, but that means that on MSVC, our testing-for-type-completeness does not work to determine whether a type has std::hash. You must also test whether operator() exists!


  1. With the notable exception of the template class having a private destructor. ↩︎

  2. This is explicitly stated in [temp.inst]/6 ↩︎

Comments

You can use your Mastodon account to reply to this post.

Reply to tinloaf's post

With an account on the Fediverse or Mastodon, you can respond to this post. Since Mastodon is decentralized, you can use your existing account hosted by another Mastodon server or compatible platform if you don't have an account on this one.

Copy and paste this URL into the search field of your favourite Fediverse app or the web interface of your Mastodon server.