How to Make SFINAE Pretty and Robust
Today we have a guest post by Ádám Balázs. Ádám is a software engineer at Verizon Smart Communities Hungary developing video analytics for embedded systems. One of his passions is compile time optimizations so he immediately agreed to write a guest post on this topic. You can find Ádám online on LinkedIn.
In the series on how to make SFINAE pretty, we saw how to make our SFINAE-boilerplate quite short and expressive.
Just take a look at its original form:
template<typename T> class MyClass { public: void MyClass(T const& x){} template<typename T_ = T> void f(T&& x, typename std::enable_if<!std::is_reference<T_>::value, std::nullptr_t>::type = nullptr){} };
And compare it with this more expressive form:
template<typename T> using IsNotReference = std::enable_if_t<!std::is_reference_v<T>>; template<typename T> class MyClass { public: void f(T const& x){} template<typename T_ = T, typename = IsNotReference <T_>> void f(T&& x){} };
We could reasonably think that we can lay back and start using it in production. We could, it works in most of the cases, but–as we are speaking about interfaces–our code must be secure and robust. Is it? Let’s try to hack it!
Flaw #1: SFINAE can be bypassed
It is common to use SFINAE to disable a piece of code depending on a condition. It can be really useful if we need to implement for example a custom abs function for whatever reason (custom arithmetic class, hardware specific optimization, educational purpose, etc…):
template< typename T > T myAbs( T val ) { return( ( val <= -1 ) ? -val : val ); } int main() { int a{ std::numeric_limits< int >::max() }; std::cout << "a: " << a << " myAbs( a ): " << myAbs( a ) << std::endl; }
This program outputs this, which looks OK:
a: 2147483647 myAbs( a ): 2147483647
But we can call our abs function with unsigned T
arguments, the effect is disastrous:
int main() { unsigned int a{ std::numeric_limits< unsigned int >::max() }; std::cout << "a: " << a << " myAbs( a ): " << myAbs( a ) << std::endl; }
Indeed, the program now outputs:
a: 4294967295 myAbs( a ): 1
Our function wasn’t designed to deal with unsigned arguments, so we should restrict the possible set of T
s with SFINAE:
template< typename T > using IsSigned = std::enable_if_t< std::is_signed_v< T > >; template< typename T, typename = IsSigned< T > > T myAbs( T val ) { return( ( val <= -1 ) ? -val : val ); }
The code works as expected: calling myAbs
with an unsigned type raises a compile time error like this:
candidate template ignored: requirement 'std::is_signed_v<unsigned int>' was not satisfied [with T = unsigned int]
Hacking the SFINAE condition
Then what’s wrong with this function? To answer this question we should check how myAbs
utilizes SFINAE.
template< typename T, typename = IsSigned< T > > T myAbs( T val );
myAbs
is a function template with two template parameter types to deduce. The first one is the type of the actual function argument, the second one is an anonymous type defaulted to IsSigned< T >
(aka. std::enable_if_t< std::is_signed_v< T > >
aka. std::enable_if< std::is_signed_v< T >, void >::type
which is void or substitution failure).
How can we call myAbs
? There are 3 ways:
int a{ myAbs( -5 ) }; int b{ myAbs< int >( -5 ) }; int c{ myAbs< int, void >( -5 ) };
The first and second calls are straightforward, but the third seems to be a bit interesting: whoa, what is that void
template argument?
The second template parameter is anonymous, has a default type but it is still a template parameter so it can be explicitly specified. Is this a problem? In this case this is a huge problem, indeed. We can use the third form to bypass our SFINAE check:
unsigned int d{ myAbs< unsigned int, void >( 5u ) }; unsigned int e{ myAbs< unsigned int, void >( std::numeric_limits< unsigned int >::max() ) };
This code compiles fine but gets the disastrous results we used SFINAE to avoid:
a: 4294967295 myAbs( a ): 1
We will solve this issue–but first: any other problems? Well…
Flaw #2: We can’t have specific implementations
Another common usage of SFINAE is to provide specific implementations for certain compile time conditions. What if we don’t want to completely forbid calling myAbs
with unsigned values but provide a trivial implementation for those cases? We could use if constexpr in C++17 (we will cover it later), or we can write something like the following:
template< typename T > using IsSigned = std::enable_if_t< std::is_signed_v< T > >; template< typename T > using IsUnsigned = std::enable_if_t< std::is_unsigned_v< T > >; template< typename T, typename = IsSigned< T > > T myAbs( T val ) { return( ( val <= -1 ) ? -val : val ); } template< typename T, typename = IsUnsigned< T > > T myAbs( T val ) { return val; }
But what’s this?
error: template parameter redefines default argument template< typename T, typename = IsUnsigned< T > > note: previous default template argument defined here template< typename T, typename = IsSigned< T > >
Oh-uh, C++ standard (C++17; §17.1.16) says the following:
“A template-parameter shall not be given default arguments by two different declarations in the same scope.”
Oops, this is exactly what we did…
Why not use a regular if statement?
We could simply use a runtime if instead:
template< typename T > T myAbs( T val ) { if( std::is_signed_v< T > ) { return ( ( val <= -1 ) ? -val : val ); } else { return val; } }
The compiler would optimize the condition out, because if( std::is_signed_v< T > )
becomes if( true )
or if( false )
after template instantiation. Yes, with our current myAbs implementation it would work. But in general it has a huge restriction: both of if
and else
statements must be valid for each T
. What if we change our implementation a bit:
template< typename T > T myAbs( T val ) { if( std::is_signed_v< T > ) { return std::abs( val ); } else { return val; } } int main() { unsigned int a{ myAbs( 5u ) }; }
Our code would fail immediately:
error: call of overloaded ‘abs(unsigned int&)’ is ambiguous
This restriction is what SFINAE eliminates: we can write code that is valid on only a subset of Ts (in myAbs valid on unsigned types only or valid on signed types only).
The solution: another form for SFINAE
What can we do to overcome these problems? For the first problem we should force our SFINAE check no matter how users call our function. Currently our check can be bypassed when the compiler doesn’t need the default type for the second template parameter.
What if we use our SFINAE code to declare a type of a template parameter instead of providing a default type? Let’s try:
template< typename T > using IsSigned = std::enable_if_t< std::is_signed_v< T >, bool >; template< typename T, IsSigned< T > = true > T myAbs( T val ) { return( ( val <= -1 ) ? -val : val ); } int main() { //int a{ myAbs( 5u ) }; int b{ myAbs< int >( 5u ) }; //int c{ myAbs< unsigned int, true >( 5u ) }; }
We need IsSigned
to be a type other than void on valid cases, because we want to provide a default value for that type. There is no value with void
type so we must use something different: bool
, int
, enum
, nullptr_t
, etc… I use bool usually, the expressions looks meaningful in this case:
template< typename T, IsSigned< T > = true >
It works! For myAbs( 5u )
the compiler gives an error as before:
candidate template ignored: requirement 'std::is_signed_v<unsigned int>' was not satisfied [with T = unsigned int]
The second call – myAbs< int >( 5u )
– is still valid, we tell the compiler the type of T
explicitly so it will cast 5u
to int
.
Finally we cannot trick myAbs
anymore: myAbs< unsigned int, true >( 5u )
raises an error. It doesn’t matter if we provide a default value or not on the call, the SFINAE part of the expression is evaluated anyway because the compiler needs the type of the anonymous template value argument.
We can move to the next problem – but wait a minute! I think we do not redefine default argument for the same template parameter anymore 🙂 What was the original situation?
template< typename T, typename = IsUnsigned< T > > T myAbs( T val ); template< typename T, typename = IsSigned< T > > T myAbs( T val );
But now with the current code:
template< typename T, IsUnsigned< T > = true > T myAbs( T val ); template< typename T, IsSigned< T > = true > T myAbs( T val );
It looks a lot the previous code so we may think this also won’t work, but in fact this code doesn’t have the same problem. What is IsUnsigned< T >
? Bool or substitution failure. And what is IsSigned< T >
? Just the same, but if one of them is bool
the other is substitution failure.
What this means is we do not redefine default arguments, since there is only one function with bool template argument – the other is substitution failure therefore it does not exist.
Syntactic sugar
EDIT: this section has been removed from the initial post, after a discussion in the comment showed its code was ill-formed. Thanks to all that participated.
Older versions of C++
All the above works from C++11, the only difference is the verbosity of the constraint definitions between the standard-specific versions:
//C++11 template< typename T > using IsSigned = typename std::enable_if< std::is_signed< T >::value, bool >::type; //C++14 - std::enable_if_t template< typename T > using IsSigned = std::enable_if_t< std::is_signed< T >::value, bool >; //C++17 - std::is_signed_v template< typename T > using IsSigned = std::enable_if_t< std::is_signed_v< T >, bool >;
But the boilerplate remains the same:
template< typename T, IsSigned< T > = true >
In good old C++98 there are no alias templates, in addition function templates cannot have default types or values. We can inject our SFINAE code into the result type or into the function parameter list only. The second option is recommended because constructors don’t have result types. The best we can do is something like this:
template< typename T > T myAbs( T val, typename my_enable_if< my_is_signed< T >::value, bool >::type = true ) { return( ( val <= -1 ) ? -val : val ); }
Just for the comparison the modern C++ version again:
template< typename T, IsSigned< T > = true > T myAbs( T val ) { return( ( val <= -1 ) ? -val : val ); }
The C++98 version is ugly, introduces a meaningless parameter but it works – you can use it if desperately needed. Oh, yes: my_enable_if
and my_is_signed
need to be implemented (std::enable_if
and std::is_signed
were new in C++11).
State of the art
C++17 introduced if constexpr
–a compile time way to discard code based on conditions. Both if and else statements must be well-formed, but the condition is going to be evaluated during the compilation.
template< typename T > T myAbs( T val ) { if constexpr( std::is_signed_v< T > ) { return( ( val <= -1 ) ? -val : val ); } else { if constexpr( std::is_unsigned_v< T > ) { return val; } /*else { static_assert( false, "T must be signed or unsigned arithmetic type." ); }*/ } }
As we can see our abs function became more compact and easier to read. However handling non-conforming types is not straightforward. The outcommented unconditional static_assert
makes that else statement ill-formed which is forbidden by the standard no matter it is going to be discarded or not.
Fortunately there is a loophole: in templated entities the discarded statements aren’t instantiated if the condition is not value-dependent. Perfect!
So the only problem with our code is that it fails at template definition time. If we could delay the evaluation of static_assert
until template instantiation time the problem would be solved: it would be instantiated if and only if all of our conditions are false. But how can we postpone static_assert
until template instantiation? Make its condition type-dependent!
template< typename > inline constexpr bool dependent_false_v{ false }; template< typename T > T myAbs( T val ) { if constexpr( std::is_signed_v< T > ) { return( ( val <= -1 ) ? -val : val ); } else { if constexpr( std::is_unsigned_v< T > ) { return val; } else { static_assert( dependent_false_v< T >, "Unsupported type" ); } } }
About the future
We are really close now, but we must wait until C++20 brings the ultimate solution: concepts! It will completely change the way we use templates (and SFINAE).
In a nutshell concepts can be used to restrict the set of arguments that are accepted for template parameters. For our abs function we could use the following concept:
template< typename T > concept bool Arithmetic() { return std::is_arithmetic_v< T >; }
And how can we use concepts? There are three ways:
// Verbose version template< typename T > requires Arithmetic< T >() T myAbs( T val ); // Short version template< Arithmetic T > T myAbs( T val ); // WOW Arithmetic myAbs( Arithmetic val );
Please note that the third form still declares a template function! Here is the complete implementation of myAbs in C++20:
template< typename T > concept bool Arithmetic() { return std::is_arithmetic_v< T >; } Arithmetic myAbs( Arithmetic val ) { if constexpr( std::is_signed_v< decltype( val ) > ) { return( ( val <= -1 ) ? -val : val ); } else { return val; } } int main() { unsigned int a{ myAbs( 5u ) }; int b{ myAbs< int >( 5u ) }; //std::string c{ myAbs( "d" ) }; }
The outcommented call raises the following error:
error: cannot call function 'auto myAbs(auto:1) [with auto:1 = const char*]' constraints not satisfied within 'template<class T> concept bool Arithmetic() [with T = const char*]' concept bool Arithmetic(){ ^~~~~~~~~~ 'std::is_arithmetic_v' evaluated to false
I encourage everyone to use these techniques in production code bravely, compile time is cheaper than runtime. Happy SFINAEing!
You will also like
- How to Make SFINAE Pretty – Part 1: What SFINAE Brings to Code
- How to Make SFINAE Pretty – Part 2: the Hidden Beauty of SFINAE
- How to Define a Variadic Number of Arguments of the Same Type
- Expressive C++ Template Metaprogramming
Share this post!