What is SFINAE in C++?
Can you please explain it in words understandable to a programmer who is not versed in C++? Also, what concept in a language like Python does SFINAE correspond to?
What is SFINAE in C++?
Can you please explain it in words understandable to a programmer who is not versed in C++? Also, what concept in a language like Python does SFINAE correspond to?
There is nothing in Python that remotely resembles SFINAE. Python has no templates, and certainly no parameter-based function resolution as occurs when resolving template specialisations. Function lookup is done purely by name in Python.
Python won't help you at all. But you do say you're already basically familiar with templates.
The most fundamental SFINAE construct is usage of
enable_if
. The only tricky part is thatclass enable_if
does not encapsulate SFINAE, it merely exposes it.In SFINAE, there is some structure which sets up an error condition (
class enable_if
here) and a number of parallel, otherwise conflicting definitions. Some error occurs in all but one definition, which the compiler picks and uses without complaining about the others.What kinds of errors are acceptable is a major detail which has only recently been standardized, but you don't seem to be asking about that.
Warning: this is a really long explanation, but hopefully it really explains not only what SFINAE does, but gives some idea of when and why you might use it.
Okay, to explain this we probably need to back up and explain templates a bit. As we all know, Python uses what's commonly referred to as duck typing -- for example, when you invoke a function, you can pass an object X to that function as long as X provides all the operations used by the function.
In C++, a normal (non-template) function requires that you specify the type of a parameter. If you defined a function like:
You can only apply that function to an
int
. The fact that it usesx
in a way that could just as well apply to other types likelong
orfloat
makes no difference -- it only applies to anint
anyway.To get something closer to Python's duck typing, you can create a template instead:
Now our
plus1
is a lot more like it would be in Python -- in particular, we can invoke it equally well to an objectx
of any type for whichx + 1
is defined.Now, consider, for example, that we want to write some objects out to a stream. Unfortunately, some of those objects get written to a stream using
stream << object
, but others useobject.write(stream);
instead. We want to be able to handle either one without the user having to specify which. Now, template specialization allows us to write the specialized template, so if it was one type that used theobject.write(stream)
syntax, we could do something like:That's fine for one type, and if we wanted to badly enough we could add more specializations for all the types that don't support
stream << object
-- but as soon as (for example) the user adds a new type that doesn't supportstream << object
, things break again.What we want is a way to use the first specialization for any object that supports
stream << object;
, but the second for anything else (though we might sometime want to add a third for objects that usex.print(stream);
instead).We can use SFINAE to make that determination. To do that, we typically rely on a couple of other oddball details of C++. One is to use the
sizeof
operator.sizeof
determines the size of a type or an expression, but it does so entirely at compile time by looking at the types involved, without evaluating the expression itself. For example, if I have something like:I can use
sizeof(func())
. In this case,func()
returns anint
, sosizeof(func())
is equivalent tosizeof(int)
.The second interesting item that's frequently used is the fact that the size of an array must to be positive, not zero.
Now, putting those together, we can do something like this:
Here we have two overloads of
test
. The second of these takes a variable argument list (the...
) which means it can match any type -- but it's also the last choice the compiler will make in selecting an overload, so it'll only match if the first one does not. The other overload oftest
is a bit more interesting: it defines a function that takes one parameter: an array of pointers to functions that returnchar
, where the size of the array is (in essence)sizeof(stream << object)
. Ifstream << object
isn't a valid expression, thesizeof
will yield 0, which means we've created an array of size zero, which isn't allowed. This is where the SFINAE itself comes into the picture. Attempting to substitute the type that doesn't supportoperator<<
forU
would fail, because it would produce a zero-sized array. But, that's not an error -- it just means that function is eliminated from the overload set. Therefore, the other function is the only one that can be used in such a case.That then gets used in the
enum
expression below -- it looks at the return value from the selected overload oftest
and checks whether it's equal to 1 (if it is, it means the function returningchar
was selected, but otherwise, the function returninglong
was selected).The result is that
has_inserter<type>::value
will bel
if we could usesome_ostream << object;
would compile, and0
if it wouldn't. We can then use that value to control template specialization to pick the right way to write out the value for a particular type.SFINAE is a principle a C++ compiler uses to filter out some templated function overloads during overload resolution (1)
When the compiler resolves a particular function call, it considers a set of available function and function template declarations to find out which one will be used. Basically, there are two mechanisms to do it. One can be described as syntactic. Given declarations:
resolving
f((int)1)
will remove versions 2 and three, becauseint
is not equal tocomplex<T>
orT*
for someT
. Similarly,f(std::complex<float>(1))
would remove the second variant andf((int*)&x)
would remove the third. The compiler does this by trying to deduce the template parameters from the function arguments. If deduction fails (as inT*
againstint
), the overload is discarded.The reason we want this is obvious - we may want to do slightly different things for different types (eg. an absolute value of a complex is computed by
x*conj(x)
and yields a real number, not a complex number, which is different from the computation for floats).If you have done some declarative programming before, this mechanism is similar to (Haskell):
The way C++ takes this further is that the deduction may fail even when the deduced types are OK, but back substitution into the other yield some "nonsensical" result (more on that later). For example:
when deducing
f('c')
(we call with a single argument, because the second argument is implicit):T
againstchar
which yields triviallyT
aschar
T
s in the declaration aschar
s. This yieldsvoid f(char t, int(*)[sizeof(char)-sizeof(int)] = 0)
.int [sizeof(char)-sizeof(int)]
. The size of this array may be eg. -3 (depending on your platform).<= 0
are invalid, so the compiler discards the overload. Substitution Failure Is Not An Error, the compiler won't reject the program.In the end, if more than one function overload remains, the compiler uses conversion sequences comparison and partial ordering of templates to select one that is the "best".
There are more such "nonsensical" results that work like this, they are enumerated in a list in the standard (C++03). In C++0x, the realm of SFINAE is extended to almost any type error.
I won't write an extensive list of SFINAE errors, but some of the most popular are:
typename T::type
forT = int
orT = A
whereA
is a class without a nested type calledtype
.int C::*
forC = int
This mechanism is not similar to anything in other programming languages I know of. If you were to do a similar thing in Haskell, you'd use guards which are more powerful, but impossible in C++.
1: or partial template specializations when talking about class templates
If you have some overloaded template functions, some of the possible candidates for use may fail to be compilable when template substitution is performed, because the thing being substituted may not have the correct behaviour. This is not considered to be a programming error, the failed templates are simply removed from the set available for that particular parameter.
I have no idea if Python has a similar feature, and don't really see why a non-C++ programmer should care about this feature. But if you want to learn more about templates, the best book on them is C++ Templates: The Complete Guide.