How to make SFINAE sleek and reliable

Hello again. We are sharing with you an interesting article, the translation of which was prepared specifically for students of the course "C ++ Developer" .










Today we have a guest post by dΓ‘m BalΓ‘zs. Adam is a software engineer at Verizon Smart Communities Hungary and is developing video analytics for embedded systems. One of his passions is optimization of compilation time, so he immediately agreed to write a guest post on this topic. You can find Adam online on LinkedIn .



In a series of articles on how to make SFINAE elegant , we saw how to make our SFINAE template pretty concise and expressive .



Just 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 can reasonably believe that it is already possible to relax and start using it in production. We could, it works in most cases, but - as we talk about interfaces - our code should be safe and reliable. Is it so? Let's try to hack it!



Flaw # 1: SFINAE Can Be Bypassed



Typically, SFINAE is used to disable part of the code depending on the condition. This can be very useful if we need to implement, for example, the user-defined function abs for any reason (user-defined arithmetic class, optimization for a specific equipment, for training purposes, 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 displays the following, which looks quite normal:



 a: 2147483647 myAbs( a ): 2147483647
      
      





But we can call our abs



function with unsigned arguments T



, and the effect will be catastrophic:



 nt main() { unsigned int a{ std::numeric_limits< unsigned int >::max() }; std::cout << "a: " << a << " myAbs( a ): " << myAbs( a ) << std::endl; }
      
      





Indeed, now the program displays:



a: 4294967295 myAbs( a ): 1







Our function was not designed to work with unsigned arguments, so we must limit the possible set of T



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: a call to myAbs with an unsigned type causes a compile-time error:



candidate template ignored: requirement 'std::is_signed_v<



unsigned int>' was not satisfied [with T = unsigned int]








Hacking SFINAE State



Then what is wrong with this function? To answer this question, we need to check how myAbs implements SFINAE.



 template< typename T, typename = IsSigned<T> > T myAbs( T val );
      
      





myAbs



is a function template with two types of input template parameters. The first is the actual type of the function argument, the second is the default anonymous type IsSigned <



T >







(otherwise std::enable_if_t <



std::is_signed_v <



T >



>




or else std::enable_if <



std::is_signed_v <



T>, void>::type




, which is void



or failed substitution).



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 is interesting: what is the argument of the void



template?



The second template parameter is anonymous, has a default type, but it is still a template parameter, so you can explicitly specify it. Is this a problem? In this case, this is really a huge problem. We can use the third form to get around 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 leads to disastrous results, to avoid which we used SFINAE:



 a: 4294967295 myAbs( a ): 1
      
      





We will solve this problem - but first: are there any other disadvantages? Well…



Flaw # 2: We Can't Have Specific Implementations



Another common use of SFINAE is to provide specific implementations for specific compile-time conditions. What if we don’t want to completely ban the call of myAbs



with myAbs



values ​​and provide a trivial implementation for these cases? We can use if constexpr in C ++ 17 (we will discuss this later), or we can:



  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 is it?



 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, the C ++ standard (C ++ 17; Β§17.1.16) states the following :



β€œThe default arguments should not be provided to the template parameter by two different declarations in the same scope.”


Oops, this is exactly what we did ...



Why not use regular if?



We could just use if at runtime 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 because if (std::is_signed_v <



T>)




becomes if (true)



or if (false)



after creating the template. Yes, with our current implementation of myAbs



this will work. But overall, this imposes a huge limitation: the if



and else



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 will immediately crash:



 error: call of overloaded 'abs(unsigned int&)' is ambiguous
      
      





This restriction is what SFINAE eliminates: we can write code that is valid only for a subset of T (in myAbs it is valid only for unsigned types or valid only for signed types).



Solution: another form for SFINAE



What can we do to overcome these shortcomings? For the first problem, we must force our SFINAE check regardless of how users invoke our function. Currently, our test can be circumvented when the compiler does not need the default type for the second template parameter.



What if we use our SFINAE code to declare a template parameter type 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 in valid cases, because we want to provide a default value for this type. There is no value for the void type, so we should use something else: bool, int, enum, nullptr_t, etc. Usually I use bool - in this case the expressions look meaningful:



 template< typename T, IsSigned< T > = true >
      
      





It is working! For myAbs (5u)



compiler throws 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 type T



explicitly, so it converts 5u



to int



.



Finally, we can no longer trace myAbs



around the finger: myAbs <



unsigned int, true> (5u)




throws an error. It doesn't matter if we provide a default value in the call or not, part of the SFINAE expression is evaluated anyway, because the compiler needs an argument type of an anonymous template value.



We can move on to the next problem - but wait a minute! I think we no longer override the default argument for the same template parameter. 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 very similar to the previous code, so we might think that this will not work either, but in fact this code does not have the same problem. What is IsUnsigned <



T>




? Bool or failed lookup. And what is IsSigned <



T>




? Same thing, but if one of them is Bool, the other is a failed lookup.



This means that we do not override the default arguments, since there is only one function with the template argument bool, the other is a failed substitution, so it does not exist.



Syntactic sugar



UPD . This paragraph was deleted by the author due to errors found in it.



Old versions of C ++



All of the above works with C ++ 11, the only difference is the verbosity of the definitions of restrictions between standard 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 template remains the same:



 template< typename T, IsSigned< T > = true >
      
      





In the good old C ++ 98, there are no template aliases, in addition, function templates cannot have types or default values. We can insert our SFINAE code into the result type or only into the list of function parameters. The second option is recommended because the constructors do not 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 comparison - the modern version of C ++:



 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 it is absolutely necessary. And yes: my_enable_if



and my_is_signed



should be implemented ( std :: enable_if std :: is_signed



were new in C ++ 11).



Current state



C ++ 17 introduced if constexpr



, a method for discarding code based on conditions at compile time. Both if and else statements must be syntactically correct, but the condition will be evaluated at compile time.



 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 has become more compact and easy to read. However, the handling of inappropriate types is not straightforward. The static_assert



out unconditional static_assert



makes this statement poorly consistent, which is prohibited by the standard, regardless of whether it is discarded or not.



Fortunately, there is a loophole: in template objects, discarded operators are not created if the condition is independent of the value. Fine!



So the only problem with our code is that it crashes during template definition. If we could defer the evaluation of static_assert



until the time the template was created, the problem would be solved: it would be created if and only if all our conditions are false. But how can we defer static_assert



until the template is created? Make its condition dependent on the type!



 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 future



We are really close already, but we need to wait a little while until C ++ 20 brings the final solution: concepts! This will completely change the way templates (and SFINAE) are used.



In a nutshell: concepts can be used to limit 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:



 //   template< typename T > requires Arithmetic< T >() T myAbs( T val ); //   template< Arithmetic T > T myAbs( T val ); //  Arithmetic myAbs( Arithmetic val );
      
      





Note that the third form still declares a template function! Here is the full 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" ) }; }
      
      





A commented out call gives 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 urge everyone to boldly use these methods in production code; compilation time is cheaper than runtime. Happy SFINAEing!



All Articles