Specializing in Generalities

I’m posting this even though I fear this blog becoming a stupid C++ trick of the week1 blog. The trick this week is detecting when a given type has a specialization for a given template. In this case there is no default implementation of the template, in order to use a type in the template the type must have a specialization. For example

template <typename T>
struct Temp;          //NOTE: No struct definition

template <>
struct Temp<int>      //Temp now works with int        
   //stuff that doesn't matter in this discussion

Now given the above we would get a compiler error if we tried to use Temp<float> without creating an explicit specialization for it (or one that works for any floating point type, see below). So really we get the detection that I’m looking for for almost free. It’s almost free, but not completely free, in the sense that the compiler will catch the problem and inform the user about the error. But it will do so in a very verbose and not altogether clear manner. Instead, I’d like to be able to detect the missing specialization in a static_assert and get a much clearer error message.

In order to do this we use SFINAE, specifically the expression variety2. SFINAE is short for substitution failure is not an error which means that in some contexts failing to be able to substitute types in templates does not lead to a compiler error. Part of the context is that there needs to be some sort of overload resolution going on, that way the compiler can ignore the overload with the substitution failure and use a different overload. There are lots of contexts where this is true (see here for an exhaustive list), we’re going to be exploiting a template parameter with a default type.

template <typename Type_t, typename = std::void_t<> >
struct HasSpec : public std::false_type

template <typename Type_t>
struct HasSpec<Type_t, std::void_t<decltype (Temp<Type_t>())>> : public std::true_type

Here we’ve created a template HasSpec that has two parameters. The second is unnamed since we’re only going to use it to induce SFINAE. We give it a default value though, in this case std::void_t<>. std::void_t is a strange beast that exists solely to make it easier to use SFINAE. The simplest definition3 is

template< class... >
using void_t = void;

It evaluates to void for any set of template parameters you give it. So how does it do anything in the above example? Note that it will always evaluate to void for the HasSpec default implementation. But it will also evaluate to void in the HasSpec specialization, if Temp<Type_t>() is a valid expression that is. What does it evaluate to if there is no specialization? Well, it doesn’t evaluate at all. this causes SFINAE to kick in, and the specialization is ignored. Since the compiler prefers the most specialized alternative when doing overload resolution it will pick the HasSpec specialization when Type_t has a Temp specialization and the default implementation when there is no Temp specialization (since the HasSpec specialization has been removed from the set of possible overloads). Since the default and specialized implementations derive from std::false_type and std::true_type respectively we can now use HasSpec with static_assert when we want to check that Temp has been specialized for a given type.

template <typename Type_t>
void UsesTemp(const Type_t& theValue)
   static_assert (HasSpec<Type_t>::value, "You need to specialize Temp for this type");

   //do stuff with Temp<Type_t>

The above will produce a nice, concise error message if UsesTemnp is called with a type that doesn’t have a Temp specialization.

Another neat use of SFINAE is to pick function overloads based on a type properties instead of a specific type. Say you have a function for which you want to have one overload for integral types, and another for floating point types. We’ll use SFINAE again, but this time via std::enable_if to which we give a boolean value and a type (you can skip the second parameter if void is OK with you). If the boolean value is true then std::enable_if has an embedded type called type that is the same as the type you gave (e.g. std::enable_if<true, int>::type would evaluate to int). If the boolean value is false then type is not defined. For example4

template <typename Type_t>
auto DoSomething (Type_t value) -> typename std::enable_if<std::is_integral<Type_t>::value, int>::type
   //Do integral stuff

template <typename Type_t>
auto DoSomething (Type_t value) -> typename std::enable_if<std::is_floating_point<Type_t>::value, float>::type
   //Do floating point stuff

So now, if we call DoSomething with any integral type we’ll get the first overload since std::enable_if will have a a type member, while it won’t have a type member for the second overload. So the compiler will ignore the second overload and use the first. Call it with a floating point value and the second overload’s enable_if will have type while the first does not, leading the compiler to use the second. Call it with anything else and you’ll get a compiler error since neither overload’s enable_if will have a type and both will be disqualified from overload resolution.

Now we could accomplish something similar by having overloads that accept int and float, but then we’d be truncating the type that we were actually passing to one of those types. The advantage of the std::enable_if trick above is that we don’t do any truncation, we always work with the type that is passed in.

One thing to note is that this second example will work with Visual Studio since we’re using type SFINAE instead of expression SFINAE. What’s the difference? With type SFINAE you’re only instantiating types, like std::enable_if and std::is_integral. In the other case you’re using expressions involving types, like decltype (Temp<Type_t>()) in the first example. Visual Studio 2015 has partial support for expression SFINAE as of update 1, but the partial support wasn’t up to the task of compiling the HasSpec example. Hopefully update 2 will be able to handle it as I’d like to be able to use this trick at work [Brett runs off to report the issue to Microsoft].

  1. Where weeks should be read as every couple of months if I’m being more realistic about post frequency as of late.

  2. Since we need expression SFINAE visual studio users are out of luck. Even with 2015. Hopefully this will change soon.

  3. Its the simplest, but may not work.

  4. In this example I directly use std:enable_if. If your standard library is new enough I would suggest using std::enable_if_t instead. It’s a type alias for std::enable_if<>::type that allows you to skip all the typename and ::type stuff in the example.


Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

%d bloggers like this: