Template SFINAE & type-traits
(869 words) Sat, May 21, 2016Today’s post is about template SFINAE & type-traits - cool C++ features with great compile-time power.
Template SFINAE
SFINAE is a scary-looking C++ acronym, which joins a long list of hard-to-remember capital-letter concepts (such as RAII, RVO, RTTI, PIMPL, etc).
SFINAE stands for “Substitution Failure In Not An Error”. Simply put, it means that when a compiler fails to substitute a template parameter it should continue looking instead of giving up.
Here’s a quick example:
#include <iostream>
using namespace std;
template <typename T> void foo(typename T::type) { cout << "1st" << endl; }
template <typename T> void foo(T) { cout << "2nd" << endl; }
struct MyStruct {
using type = int;
};
int main() {
foo<MyStruct>(2); // ok - calls first version
foo<int>(2); // also ok - calls second version
}
Output:
1st
2nd
One important thing to note is that SFINAE, like its name suggests, only happens during substitution. Essentially it means that, like above, providing an error during function matching (aka overload resolution) is OK and the compiler will continue on its journey to find the proper function. However, failure inside a function’s body will yield an unrecoverable compiler error, afterwhich the compiler will not continue to search for other functions.
Here’s an example. If you don’t know what forward references are, you can safely
ignore the second version of zoo()
; All you need to know is that both
functions could match on any parameter passed.
template <typename Container>
void zoo(const Container& container) { // 1st version
auto it = container.begin();
}
template <typename NonContainer>
void zoo(NonContainer&& non_container) {} // 2nd version
int main() {
std::less<int> l;
// take a const-reference to ensure 1st version is called; If we used 'l'
// the 2nd version would be preferred with NonContainer == std::less<int>&
const std::less<int>& r = l;
zoo(r); // ERROR: no member named 'begin' in 'std::less<int>';
// Compiler will *not* attempt to use the 2nd version of zoo
}
Type Traits
SFINAE has already been in C++98. C++11 introduces a new feature, not directly related but often used together: template-partial-specialization (which might have its own post at a later point). By combining these 2 features together one can create some powerful tricks.
Here’s a simple implementation of std::enable_if
(found in <type_traits>
):
// this is an actual complete implementation of std::enable_if found in std
// header <type_traits>
template <bool Condition, typename T = void>
struct enable_if {
// No 'type' here, so any attempt to use it will fail substitution
};
// partial specialization for when Condition==true
template <typename T>
struct enable_if<true, T> {
using type = T;
};
This simple-but-powerful struct
allows us to set compile-time conditions on
our functions and classes. The way one uses enable_if
is by specifying a
condition as the first template-parameter, and a type T
(optional) that will
be used if the condition is true
. Take a minute to think about it as it’s not
trivial.
Here’s a simple example of a function bar()
which only accepts arguments that
are enum
s. Attempting to pass non-enum
will fail compilation:
// T must be an enum type.
// Second template argument is only used to enforce T's type, therefore it
// doesn't have a name and it is not used.
template <typename T,
typename = typename enable_if<std::is_enum<T>::value, void>::type>
void bar(T t) {}
enum Enum1 { A, B };
enum class Enum2 { C, D };
int main() {
bar(A);
bar(Enum2::C);
bar(1); // compile error - "no matching function for call to 'bar(int)'"
}
Another way to achieve basically the same thing is as follows:
// If T is enum - return value is void, otherwise - substitution failure.
template <typename T>
typename std::enable_if<std::is_enum<T>::value, void>::type bar2(T t) {}
// Rest unchanged.
Here’s a summary of the differences between bar()
and bar2()
:
bar() | bar2() |
---|---|
2 template arguments | 1 template argument |
Simple void return value |
Conditional, harder to understand return-value |
Bypassable “security” (by specifying 2nd template argument) | Unbreakable |
Requires additional template parameter | No additional parameters |
Please note that the above bar*()
s could have been implemented using
static_assert()
instead of enable_if
. Choose the right tool for the job.
Here’s a quick comparison:
enable_if |
static_assert() |
---|---|
Allows complicated overload rules without template specialization | Forces compilation to fail when a criteria was not met |
Takes place during function matching | Only takes place after overload resolution, when a function has been selected |
Long, harder to parse compilation errors | Compile error is user-defined (2nd parameter to static_assert() ) |
For more awesome type traits check out the standard C++ header <type_traits>
- it’s full of goodies you might find useful. Some traits are even impossible to
implement in C++, and require special compiler support (like
has_virtual_destructor
).
Try it out!
5 Minute Practice (use cpp.sh if you don’t have a compiler handy - it’s an online C++ compiler)
As a practice, try to implement the following classes (they all should define
static constexpr bool value
as true
or false
, according to their names:
std::is_pointer<T>
-value
istrue
ifT
is a pointer,false
otherwisestd::is_const<T>
-value
istrue
ifT
hasconst
qualifier,false
otherwisestd::is_void<T>
-value
istrue
ifT
isvoid
,false
otherwisestd::is_same<T, U>
-value
istrue
ifT
is of the same type asU
,false
otherwise