WTF is SFINAE?
SFINAE is a Latin word derived from the expression homo faber suae quisque sfinae.
I’m just kidding: it’s an acronym that stands for the uninformative sentence Substitution Failure Is Not An Error. Let’s take a quick look at what this means.
SWASEINABT
(Starting With A Simple Example Is Not A Bad Thing)
Try to guess what the example below does.
#include <stdexcept>
#include <iostream>
#include <vector>
#include <list>
using namespace std; // On small examples, your sins are forgiven
// alias for "return type of T[n]"
template<typename T>
using access_t = decltype(T::operator[](declval<typename T::value_type>()));
// use this if T[n] is a valid expression...
template<typename T, typename = access_t<T>>
typename T::value_type at(const T& container, size_t n) {
return container[n];
}
// ...otherwise, use this
template<typename T>
typename T::value_type at(const T& container, size_t n) {
size_t i = 0;
for(auto it = container.begin(); it != container.end(); ++it) {
if (i == n) return *it;
++i;
}
throw runtime_error("Out of bounds");
}
int main() {
vector<int> a {1, 3, 5, 9, 1, 6};
list<int> b {1, 4, 0, 8, 7};
cout << "a[4] == " << at(a, 4)
<< "\nb[2] == " << at(b, 2) << endl;
// Output:
// a[4] == 1
// b[2] == 0
}
This code snippet is defining the function at(container, n)
,
which returns the n
-th element of container
. If the container supports
indexing with the []
operator, it uses the first version of at
. Otherwise,
it uses the second version. In C++, vector
s support []
, but list
s don’t,
so we should see both functions being used here.
Let’s walk through the tricky bits:
-
The
using
declaration at the beginning defines a “metafunction”, which in this case is really just an alias for the return type ofT::operator[]
.declval
is a sort of dummy value, used to evaluate what typeT::operator[]
will return.decltype
evaluates the compile-time type of the expression. -
The signature starting with
template<typename T, typename = access_t<T>>
looks strange: why is there a default, unnamed parameter? This is really the essence of SFINAE:access_t<T>
needs to be well-defined, and this will only be the case if the typeT
has anoperator[]
function. If it doesn’t, then Substitution Failure will occurr, however this Is Not An Error, because there is anotherat
function which is well-defined and can be used instead.
In C++20 you no longer need the hack with access_t<T>
: you can use concepts
and contraints to the same effect.
template<typename T>
concept Indexable = requires(T t) {
T::operator[];
};
template<Indexable T>
typename T::value_type at(const T& container, size_t n) {
return container[n];
}
template<typename T>
typename T::value_type at(const T& container, size_t n) {
size_t i = 0;
for(auto it = container.begin(); it != container.end(); ++it) {
if (i == n) return *it;
++i;
}
throw runtime_error("Out of bounds");
}
That’s the essence of it. Of course, there is a lot more to SFINAE, from void_t
to enable_if
, to
this abstruse example from cppreference.com:
template<int I>
void div(char(*)[I % 2 == 0] = nullptr)
{
// this overload is selected when I is even
}
template<int I>
void div(char(*)[I % 2 == 1] = nullptr)
{
// this overload is selected when I is odd
}
In fact, I wanted to write a much longer article about SFINAE “from first principles”, but then realized that it was slowly turning into a full-blown primer on template metaprogramming that I didn’t have time to write. If you’re interested, here’s some further reading material.
Further reading
- https://www.cppstories.com/2016/02/notes-on-c-sfinae/
- https://github.com/Pharap/CppExplanations/blob/master/SFINAE.md
- https://en.cppreference.com/w/cpp/language/sfinae
- https://en.cppreference.com/w/cpp/types/void_t
- https://en.cppreference.com/w/cpp/types/enable_if
- https://en.cppreference.com/w/cpp/language/constraints