SFINAE in C++11 and beyond
As C++ coder, sooner or later you will encounter elements of meta-programming. It might be as simple a STL container. Or it might be a full blown template class that takes variadic arguments. I, myself, have also taken this journey. As I ventured deeper into meta-programming, acronyms such as CRTP, SFINAE really piqued my interest. Today, let’s talk about SFINAE.
SFINAE stands for “substitution failure is not an error”, and there has been numerous articles, stack overflow questions and blog posts on this topic:
- SFINAE introduction by Jean Guegant
- cppreference.com SFINAE
- SFINAE and enable if by Eli Bendersky
My goal here is not to regurgitate what these excellent resources provide, but to summarize it in a concise way for my own learning and interpretation.
What problem does it solve?
SFINAE is an idiom, or a programming pattern in C++. At its core, it enables compile time introspection of template parameters for more compact and generalizable code that can be reused later. They are frequently associated template meta-programming - so you should think about when they might come in handy if you work with a lot of templated code.
Through SFINAE, we can write methods / struct / meta-helpers that help us answer questions like:
- Does the template argument
T
passed into this class at compile-time implement the methodfoo()
? - I want to have two
template<int N> Array
implementation (instead of having two different classesSmallArray
,LargeArray
) depending on the size ofN
. - I want to have my template function
template<typename T> foo(T val)
behave differently depending on some condition onT
, such as if it’s afloat
type or anint
type.
I also think of SFINAE pattern like function overload lookup on steroids, but also extended and applicable for classes, methods etc.
Prior to C++11, SFINAE implementations mostly uses template overload rules and clever use of the side-effects sizeof
and typedefs
to achieve this goal. With C++11’s introduction of std::decltype
, std::declval
, and std::enable_if
, the SFINAE pattern becomes a little easier to read and understand.
Case Study
Source: Stack Overflow
C++11 switching f(T) based on whether T is an integer or a float
template<typename T>
std::enable_if_t<std::is_integral<T>::value> f(T t){
//integral version
}
template<typename T>
std::enable_if_t<std::is_floating_point<T>::value> f(T t){
//floating point version
}
std::enable_if_t
will define a ::value
of type void
if the substitution is successful, otherwise, it will be undefined, and thereby the function declaration will be disabled at compile time.
To define a different return value type, we can supply our own type as the 2nd argument to std::enable_if_t
, for example: std::enable_if_t<std::is_integral<T>::value, T>
would evaluate to T only if the std::is_integral<T>
is true.
Evaluate for logical conditions in the template parameter
template<int I> void div(char(*)[I % 2 == 0] = 0) {
/* this is taken when I is even */
}
template<int I> void div(char(*)[I % 2 == 1] = 0) {
/* this is taken when I is odd */
}
In this example, we have a special syntax []
enclosing some boolean expression after the function argument declaration in parenthesis. This was a pretty obscure feature in C++ called template argument deduction. This is another compile-time evaluation feature where [I % 2 ==0]
is evaluated at compile time, and depending on what the expression evaluate to, will pick the either the even version, or the odd version.
As an aside, a pretty cool use-case of this template deduction is to extract the # of elements in an array:
template<typename T, std::size_t N>
constexpr std::size_t arraySize(T (&)[N]) noexcept
{
return N;
}
Check whether a class has a certain member of certain type
// Header
template<typename T>
struct TypeSink{
using Type = void;
};
template<typename T>
using TypeSinkT = typename TypeSink<T>::Type;
//use case
template<typename T, typename=void>
struct HasBarOfTypeInt : std::false_type{};
template<typename T>
struct HasBarOfTypeInt<T, TypeSinkT<decltype(std::declval<T&>().*(&T::bar))>> :
std::is_same<typename std::decay<decltype(std::declval<T&>().*(&T::bar))>::type,int>{};
struct S{
int bar;
};
struct K{
};
template<typename T, typename = TypeSinkT<decltype(&T::bar)>>
void print(T){
std::cout << "has bar" << std::endl;
}
void print(...){
std::cout << "no bar" << std::endl;
}
int main(){
print(S{});
print(K{});
std::cout << "bar is int: " << HasBarOfTypeInt<S>::value << std::endl;
}
This is a more complicated example, let’s work through it step by step.
First, we have a dummy struct TypeSink
, of which, the Type
in its scope is defined as a void type. The TypeSinkT
is a type alias to refer to the underlying type in the TypeSink<T>::Type
(I tried to remove it, and the compilers barfed).
HasBarOfTypeInt
is another dummy struct that derives from std::false_type{}
- which means HasBarOfTypeInt::value
will evaluate to false. The second template argument typename=void
in the struct means that the 2nd type argument will default to void type if unspecified.
In the template partial specialization of HasBarOfTypeInt<T, TypeSinkT<decltype(std::declval<T&>().*(&T::bar))>>
- the 2nd type argument is checking whether T
has the member bar inside. If it does, the TypeSink<int>::Type
is the final type. If T
does not contain bar
, then the whole expression will fail- triggering SFINAE (substitution failure is not an error) and results in the 2nd template argument evaluating to void
, this then triggers the the fallback instantiation of std::false_type
.
We also note that this template partial specialization also derives from something else: std::is_same
, is_same as explained in the official C++ reference, states that:
If T and U name the same type (taking into account const/volatile qualifications), provides the member constant value equal to true. Otherwise value is false.
Therefore, std::is_same
is checking whether the declared type of bar
is of type int. (std::decay
removes const
).
In the test code, we have partial template specialization of print()
function that takes the 2nd argument of typename = TypeSinkT<decltype(&T::bar)>
. This specialized function will get called when the typename =
evaluates to some concrete type.
Check whether a template argument (non-Type) satisfies some criteria
Source: Stack Overflow
template<bool> struct Range;
template<typename T, int N = 0, typename = Range<true>>
class Channel
{
// Implementation for all other cases of N
// also, N defaults to 0
}
template<typename T, int N>
class Channel<T, N, Range<(N <= 0)> >
{
// specialized implementation for when the N <= 0
// also the default case since N defaults to 0
}
In this example, we define a dummy templated struct Range
that has a single Boolean template argument. Depending on the value of the boolean, the Range<true>
and Range<false>
are considered two different types by the compiler.
It appears that if N is known at compile time (either a const, static, or a constexpr), then the Range<(boolean expression)>
can be evaluated at compile time. The final evaluated value of Range<>
then triggers compiler template specialization lookup rules.
At the end, the correct class is used.
Common Themes and Summary
- Template specialization and function lookup rules are what enables SFINAE. i.e. C++ compilers will look for the most “specific” definition of a function or a class to use first, if it is not found, then a more general version is used instead.
- Substitution failures is not an error - most SFINAE idioms rely on some lookup of an internal type, or some logic condition to evaluate to false - triggering a type system switch to point to some undefined type. In other words, we are intentionally triggering a failure in overload deduction - and causing the compiler to switch to the more general implementation instead
- The syntax is really confusing - for example,
decltype(std::declval<T&>().*(&T::bar))
refers to the declared type of the member variablebar
of classT
, but it is getting better with later C++ standards and better meta-programming support. - Avoid it at all costs if you do not need it. For most cases, function overloading and basic template specialization will do. You will rarely encounter application code where meta-programming on steroids is the only solution to the problem at hand.