Determining If A Template Specialization Exists

Determining If A Template Specialization Exists

By Lukas Barth

Overload, 31(173):7-10, February 2023


How do you tell if a class or function template can be used with specific arguments? Lukas Barth details his approach.

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 not entirely clear what ‘a specialization exists’ means. Even though this might seem unintuitive, I’ll postpone that discussion to the last section (‘What do I mean by ‘a specialization exists’?’) and will, for now, 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 [C++17], and I usually use GCC 12 and Clang 15 as compilers. See the sidebar on MSVC at the end for why I’m not using it in this article.

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, my proposed solution is in Listing 1.

struct HasStdSwap {
private:
  template <class T, class Dummy =
    decltype(std::swap<T>(std::declval<T &>(),
                          std::declval<T &>()))>
  static constexpr bool exists(int) {
    return true;
  }
  template <class T> static constexpr bool 
    exists(char) { return false; }
public:
  template <class T> static constexpr bool
      check() {
    return exists<T>(42); }
};
Listing 1

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 at Compiler Explorer [CompExp-1]. 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 Listing 2.

struct HasStdHash {
 private:
   template <class T, class Dummy = decltype(std::hash<T>{})>
   static constexpr bool exists(int) {
     return true;
   }
   template <class T>
   static constexpr bool exists(char) {
     return false;
   }
 public:
   template <class T>
   static constexpr bool check() {
     return exists<T>(42);
   }
 }
Listing 2

This works nicely as you can see at Compiler Explorer [CompExp-2]. This is how you can use it:

  std::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, Listing 3 is what we would end up with.

template <template <class... InnerArgs> 
           class Tmpl>
 struct IsSpecialized {
 private:
   template <class... Args, 
     class dummy = decltype(Tmpl<Args...>{})>
   static constexpr bool exists(int) {
     return true;
   }
   template <class... Args>
   static constexpr bool exists(char) {
     return false;
   }
 public:
   template <class... Args>
   static constexpr bool check() {
     return exists<Args...>(42);
   }
 };
Listing 3

This does still work for std::hash, as you can see at Compiler Explorer [CompExp-3], when being used like this:

 std::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 Listing 4 will work regardless of how the template class is defined1 (changes highlighted in Listing 4).

As an aside, if you know of a way to extend this to templates taking non-type template parameters, please let me know!

template <template <class... InnerArgs>
          class Tmpl>
struct IsSpecialized {
private:
  template <class... Args,
    class dummy =
    decltype(std::declval<Tmpl<Args...>>()
    .~Tmpl<Args...>())>
  static constexpr bool exists(int) {
    return true;
  }
  template <class... Args>
  static constexpr bool exists(char) {
     return false;
   }
 public:
   template <class... Args>
   static constexpr bool check() {
     return exists<Args...>(42);
   }
 };
Listing 4

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 [CompExp-4].

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:

  template<class T> struct SomeStruct;
  bool test1 =
    IsSpecialized<SomeStruct>::check<std::string>();
  template<> struct SomeStruct<std::string> {};
  bool 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 Listing 4, 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 [CompExp-5], 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 [C++17a]:

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, [C++17c] 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:

  auto 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?

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

Unfortunately, no. [CompExp-6] 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 paragraph 8 in section 17.8.2 (temp.deduct) of the specification [C++17d]. The unnamed class that the compiler internally creates for the generic lambda looks (simplified) something like this:

  struct Unnamed {
     template <class T1, class T2>
     auto operator()(T1 &lhs, T2 &rhs) {
       return std::swap(lhs, rhs);
     }
   };

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 the temp.deduct section of the specification [C++17d] says that types are substituted during template deduction. Quoting from paragraph 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:

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

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 on Compiler Explorer [CompExp-7] and in Listing 5.

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

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:

  template<class T>
  struct SomeStruct;
   
  template<>
  struct SomeStruct<int> {};
  // (Point A) Which specializations "exist" 
  // at this point?
  template<>
  struct 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:

  template<class T>
  void doFoo(T t);
   
  template<class T, class Dummy=
    std::enable_if_t<std::is_integral_v<T>, 
    bool> = true>
  void doBar(T t);
  template<class T, class Dummy=std::is_same_v<T,
    std::string>, bool> = true>
  void doBar(T t) {};
   
  // (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 [CompExp-8], 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 [Microsoft]:

  template <class _Kty, bool _Enabled>
  struct _Conditionally_enabled_hash 
  {
    // conditionally enabled hash base
    size_t operator()(const _Kty &_Keyval) const
    {
    return hash<_Kty>::_Do_hash(_Keyval);
    }
  };
  template <class _Kty>
  struct _Conditionally_enabled_hash<_Kty, false>
  {
    // conditionally disabled hash base
    _Conditionally_enabled_hash() = delete;
    // *no* operator()!
  };
  template <class _Kty>
  struct hash
    : _Conditionally_enabled_hash
    <_Kty, should_be_enabled_v<_Kty>>
  {
    // *no* operator()!
  };

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!

References

[C++17] The C++17 Standard: https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2017/n4659.pdf

[C++17a] Explicit specialization declaration: https://timsong-cpp.github.io/cppwp/n4659/temp.expl.spec#6

[C++17b] Implicit specialization: https://timsong-cpp.github.io/cppwp/n4659/temp.inst#6

[C++17c] Explicit specialization: https://timsong-cpp.github.io/cppwp/n4659/temp.expl.spec#7

[C++17d] Template argument deduction: https://timsong-cpp.github.io/cppwp/n4659/temp.deduct

[CompExp-1] Compiler Explorer (1): https://godbolt.org/z/MjTn6Y3Eo

[CompExp-2] Compiler Explorer (2): https://godbolt.org/z/6hv4rcTrc

[CompExp-3] Compiler Explorer (3): https://godbolt.org/z/qhso5vajj

[CompExp-4] Compiler Explorer (4): https://godbolt.org/z/8ocaWMTd9

[CompExp-5] Compiler Explorer (5): https://godbolt.org/z/5xMv6Mh16

[CompExp-6] Compiler Explorer (6): https://godbolt.org/z/jj4PjYG9n

[CompExp-7] Compiler Explorer (7): https://godbolt.org/z/MWjW7WT84

[CompExp-8] Compiler Explorer (8): https://godbolt.org/z/8bMhM5xhq

[Microsoft] MSVC STL implementation: https://github.com/microsoft/STL/blob/214e0143d1d2f7a1c5ca53a338ba3fbb657bdfa3/stl/inc/type_traits#L2177-L2204

Footnotes

  1. With the notable exception of the template class having a private destructor.
  2. This is explicitly stated in [C++17b].

This article was published on Lukas Barth’s blog on 1 January 2023 and is available at: https://lukas-barth.net/blog/checking-if-specialized/

Lukas Barth is a software engineer who has been using C++ almost exclusively for a couple of years now. After completing his computer science PhD with a focus on algorithms in 2020, he now works at MENTZ on building a journey planner for public transport.






Your Privacy

By clicking "Accept Non-Essential Cookies" you agree ACCU can store non-essential cookies on your device and disclose information in accordance with our Privacy Policy and Cookie Policy.

Current Setting: Non-Essential Cookies REJECTED


By clicking "Include Third Party Content" you agree ACCU can forward your IP address to third-party sites (such as YouTube) to enhance the information presented on this site, and that third-party sites may store cookies on your device.

Current Setting: Third Party Content EXCLUDED



Settings can be changed at any time from the Cookie Policy page.