std::enable_if
is a convenient utility to use boolean conditions to trigger SFINAE. It is defined as:
template <bool Cond, typename Result=void>
struct enable_if { };
template <typename Result>
struct enable_if<true, Result> {
using type = Result;
};
That is, enable_if<true, R>::type
is an alias for R
, whereas enable_if<false, T>::type
is ill-formed as that specialization of enable_if
does not have a type
member type.
std::enable_if
can be used to constrain templates:
int negate(int i) { return -i; }
template <class F>
auto negate(F f) { return -f(); }
Here, a call to negate(1)
would fail due to ambiguity. But the second overload is not intended to be used for integral types, so we can add:
int negate(int i) { return -i; }
template <class F, class = typename std::enable_if<!std::is_arithmetic<F>::value>::type>
auto negate(F f) { return -f(); }
Now, instantiating negate<int>
would result in a substitution failure since !std::is_arithmetic<int>::value
is false
. Due to SFINAE, this is not a hard error, this candidate is simply removed from the overload set. As a result, negate(1)
only has one single viable candidate - which is then called.
It's worth keeping in mind that std::enable_if
is a helper on top of SFINAE, but it's not what makes SFINAE work in the first place. Let's consider these two alternatives for implementing functionality similar to std::size
, i.e. an overload set size(arg)
that produces the size of a container or array:
// for containers
template<typename Cont>
auto size1(Cont const& cont) -> decltype( cont.size() );
// for arrays
template<typename Elt, std::size_t Size>
std::size_t size1(Elt const(&arr)[Size]);
// implementation omitted
template<typename Cont>
struct is_sizeable;
// for containers
template<typename Cont, std::enable_if_t<std::is_sizeable<Cont>::value, int> = 0>
auto size2(Cont const& cont);
// for arrays
template<typename Elt, std::size_t Size>
std::size_t size2(Elt const(&arr)[Size]);
Assuming that is_sizeable
is written appropriately, these two declarations should be exactly equivalent with respect to SFINAE. Which is the easiest to write, and which is the easiest to review and understand at a glance?
Now let's consider how we might want to implement arithmetic helpers that avoid signed integer overflow in favour of wrap around or modular behaviour. Which is to say that e.g. incr(i, 3)
would be the same as i += 3
save for the fact that the result would always be defined even if i
is an int
with value INT_MAX
. These are two possible alternatives:
// handle signed types
template<typename Int>
auto incr1(Int& target, Int amount)
-> std::void_t<int[static_cast<Int>(-1) < static_cast<Int>(0)]>;
// handle unsigned types by just doing target += amount
// since unsigned arithmetic already behaves as intended
template<typename Int>
auto incr1(Int& target, Int amount)
-> std::void_t<int[static_cast<Int>(0) < static_cast<Int>(-1)]>;
template<typename Int, std::enable_if_t<std::is_signed<Int>::value, int> = 0>
void incr2(Int& target, Int amount);
template<typename Int, std::enable_if_t<std::is_unsigned<Int>::value, int> = 0>
void incr2(Int& target, Int amount);
Once again which is the easiest to write, and which is the easiest to review and understand at a glance?
A strength of std::enable_if
is how it plays with refactoring and API design. If is_sizeable<Cont>::value
is meant to reflect whether cont.size()
is valid then just using the expression as it appears for size1
can be more concise, although that could depend on whether is_sizeable
would be used in several places or not. Contrast that with std::is_signed
which reflects its intention much more clearly than when its implementation leaks into the declaration of incr1
.