Comments
You can use your Mastodon account to reply to this post.
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.
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.
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>();
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.
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.
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.
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:
T
, the specialization doFoo<T>
“exists”, because the respective overload has
been declared in lines one and two.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.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.
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!
You can use your Mastodon account to reply to this post.