The purpose of the enable_if
in this case is to cause a compilation error when the deduced template argument for T
is not an integral type. We can see from cppreference/is_integral that integral types are integers, characters and their signed and unsigned variants.
For any other type you will get an error that looks like:
main.cpp:21:32: error: no matching function for call to 'is_odd'
std::cout << "i is odd: " << is_odd(NotIntegral()) << std::endl;
^~~~~~
main.cpp:6:25: note: candidate template ignored: disabled by 'enable_if' [with T = NotIntegral]
typename std::enable_if<std::is_integral<T>::value,bool>::type
I understand if it's used as a return type specifier is that the compiler will ignore the code
This isn't true. The return type is evaluated just like any other part of the declaration. See Why should I avoid std::enable_if in function signatures. The choice of placement of the std::enable_if
has its pros and cons.
But i am confused what is the purpose of that second argument?
Consider the example where you have a function named foo
that takes some T
.
template<class T> void foo(T);
You want to restrict one overload of foo
whose T
's have a value
member equal to 1
, and want to use another overload for when T::value
does not equal 1
. If we simply have this:
template<class T> void foo(T); // overload for T::value == 1
template<class T> void foo(T); // overload for T::value != 1
How does this convey to the compiler that you want to use two separate overloads for two separate things? It doesn't. They are both ambiguous function calls:
template<std::size_t N>
struct Widget : std::integral_constant<std::size_t, N> { };
int main() {
Widget<1> w1;
Widget<2> w2;
foo(w1); // Error! Ambiguous!
foo(w2); // Error! Ambiguous!
}
You will need to use SFINAE to reject the templates based on our condition:
template<class T, class = std::enable_if_t<T::value == 1>* = nullptr>
void foo(T); // #1
template<class T, class = std::enable_if_t<T::value != 1>* = nullptr>
void foo(T); // #2
Now the correct ones are called:
foo(w1); // OK! Chooses #1
foo(w2); // OK! Chooses #2
What makes this work is the way std::enable_if
is built. If the condition in its first parameter is true, it provides a member typedef named type
equal to the second parameter (defaulted to void
). Otherwise, if the condition is false, it does not provided one. A reasonable implementation could be:
template<bool, class R = void>
struct enable_if { using type = R; };
template<class R>
struct enable_if<false, R> { /* empty */ };
So if the condition fails, we will be trying to access a ::type
member that doesn't exist (remember that std::enable_if_t
is an alias for std::enable_if<...>::type
). The code would be ill-formed, but instead of it causing a hard error, during overload resolution the template is simply rejected from the candidate set and other overloads (if present) are used instead.
This is a simple explanation of SFINAE. See the cppreference page for more information.
In your case there is only one overload of is_odd
and is_even
, so if you pass NotIntegral
to them, they will fail because their respective overloads are taken out of the candidate set, and since there are no more overloads to evaluate overload resolution fails with the aforementioned error. This could be cleaned up a bit with a static_assert
message instead.
template<class T>
bool is_even (T i) {
static_assert(std::is_integral<T>::value, "T must be an integral type");
return !bool(i%2);
}
Now you will no longer be given a weird looking error message but your own custom one. This also rules out SFINAE since the static_assert
is evaluated after template arguments are substituted, so it's your choice on which route to take. I would recommend the assertion since it's much cleaner and there is no need for SFINAE here.