How Does Std::Enable_If Work

How Does std::enable_if work?

As is mentioned in comment by 40two, understanding of Substitution Failure Is Not An Error is a prerequisite for understanding std::enable_if.

std::enable_if is a specialized template defined as:

template<bool Cond, class T = void> struct enable_if {};
template<class T> struct enable_if<true, T> { typedef T type; };

The key here is in the fact that typedef T type is only defined when bool Cond is true.

Now armed with that understanding of std::enable_if it's clear that void foo(const T &bar) { isInt(bar); } is defined by:

template<typename T>
typename std::enable_if<std::numeric_limits<T>::is_integer, void>::type foo(const T &bar) { isInt(bar); }

As mentioned in firda's answer, the = 0 is a defaulting of the second template parameter. The reason for the defaulting in template<typename T, typename std::enable_if<std::is_integral<T>::value, int>::type = 0> is so that both options can be called with foo< int >( 1 );. If the std::enable_if template parameter was not defaulted, calling foo would require two template parameters, not just the int.


General note, this answer is made clearer by explicitly typing out typename std::enable_if<std::numeric_limits<T>::is_integer, void>::type but void is the default second parameter to std::enable_if, and if you have c++14 enable_if_t is a defined type and should be used. So the return type should condense to: std::enable_if_t<std::numeric_limits<T>::is_integer>

A special note for users of visual-studio prior to visual-studio-2013: Default template parameters aren't supported, so you'll only be able to use the enable_if on the function return: std::numeric_limits as a Condition

How does std::enabled_if work when enabling via a parameter

why in enable_if is only condition without indicating second template
parameter ?

Because the default void is just fine.

What type is "type*" then ? void* ? if so, why ?

Yes, ::type will be of type void if std::is_trivially_destructible<T>::value == true, this will result in ::type* -> void*.

Why is it pointer ?

So we can easily give it a default value of 0.


All we're using std::enable_if for is to check for certain attributes (in this case checking if T is trivially destructible), if these result in false then we use it to create ill-formed code and thus eliminate this function from overload resolution.

If std::is_trivially_destructible<T>::value == false then ::type will not exist and thus the code will be ill-formed. In SFINAE this is handy since this overload will then not be considered for resolution.

Why does std::enable_if require the second template type?

If the enable_if would go through, the first snippet would produce:

template<int = 0>

Which is valid.

But this, which is what you would get from snippet 2:

template<void>

Isn't and so SFINAE always kicks in here.

Understanding enable_if implementation in C++98

First consider this:

template<bool b>
struct foo {
static const bool B = b;
};

template <>
struct foo<false> {
static const bool B = false;
};

Its a primary template and a specialization. In the general case foo<b>::B is just b. In the special case when b == false the specialization kicks in and foo<false>::B is false.

Your example of std::enable_if is different for two reasons: A) It is using partial specialization. The specialization is for any type T and b == false;. B) in the specialization there is no type member alias. And thats the whole purpose of std::enable_if. When the condition is false then std::enable_if< condition, T>::type is a substitution failure, because the specialization has no type. When the condition is true then std::enable_if<condition,T>::type is just T.

Why should I avoid std::enable_if in function signatures

Put the hack in the template parameters.

The enable_if on template parameter approach has at least two advantages over the others:

  • readability: the enable_if use and the return/argument types are not merged together into one messy chunk of typename disambiguators and nested type accesses; even though the clutter of the disambiguator and nested type can be mitigated with alias templates, that would still merge two unrelated things together. The enable_if use is related to the template parameters not to the return types. Having them in the template parameters means they are closer to what matters;

  • universal applicability: constructors don't have return types, and some operators cannot have extra arguments, so neither of the other two options can be applied everywhere. Putting enable_if in a template parameter works everywhere since you can only use SFINAE on templates anyway.

For me, the readability aspect is the big motivating factor in this choice.

Why the default value is needed for `std::enable_if`?

The compiler clearly told you what the problem is: in your template declaration you specified an extra template non-type parameter that cannot be deduced. How do you expect the compiler to deduce a proper value for that non-type parameter? From what?

This is exactly why the above technique for using std::enable_if requires a default argument. This is a dummy parameter, so the default argument value does not matter (0 is a natural choice).

You can reduce your example to a mere

template <typename T, T x> 
void foo(T t) {}

int main()
{
foo(42);
}

Producing

error: no matching function for call to 'foo(int)'
note: template argument deduction/substitution failed:
note: couldn't deduce template parameter 'x'

The compiler can deduce what T is (T == int), but there's no way for the compiler to deduce the argument for x.

Your code is exactly the same, except that your second template parameter is left unnamed (no need to give a name to a dummy parameter).


Judging by your comments, you seem to be confused by the presence of keyword typename in the declaration of the second parameter in your code, which makes you believe that the second parameter is also a type parameter. The latter is not true.

Note, that in the second parameter's declaration keyword typename is used in a completely different role. This keyword simply disambiguates the semantics of

std::enable_if<std::is_class<T>::value, T>::type

It tells the compiler that nested name type actually represents a name of a type, not of something else. (You can read about this usage of typename here: Why do we need typename here? and Where and why do I have to put the "template" and "typename" keywords?)

This usage of typename does not turn the second parameter of your template into a type parameter. The second parameter of your template is still a non-type parameter.

Here's another simplified example that illustrates what happens in your code

struct S { typedef int nested_type; };

template <typename T, typename T::nested_type x>
void bar(T t)
{}

int main()
{
S s;
bar<S, 42>(s);
}

Note that even though the declaration of the second parameter begins with a typename, it still declares a non-type parameter.

Six different usages of std::enable_if in conditionally compiled templates

How should one constrain a template?

If you are not limited to compatibility with older C++ standards, and you don't need to refer to the template type, and the constraints only involve a single template parameter, prefer the least boilerplate option:

// #1
void foo(const std::convertible_to<std::string_view> auto& msg);

Otherwise, prefer the slightly more verbose form:

// #2
template <typename T>
requires std::convertible_to<T, std::string_view>
void foo(const T& msg);

The form #2 gives a name to the template type and continues to function if the constraints involve multiple template parameters. It is still not directly applicable to older C++, but the location of the constraint is compatible with older C++ enable_if usage:

// #2, compatible version

// C++11
#define TEMPLATE(...) template <__VA_ARGS__
#define REQUIRES(C) , typename std::enable_if<(C), int>::type = 0>
#define CONVERTIBLE_TO(From, To) std::is_convertible<From, To>::value

// C++20
#define TEMPLATE(...) template <__VA_ARGS__>
#define REQUIRES(C) requires (C)
#define CONVERTIBLE_TO(From, To) std::convertible_to<From, To>

TEMPLATE(typename T)
REQUIRES(CONVERTIBLE_TO(T, std::string_view))
void foo(const T& msg);

The following options are also available, but I would stick to #1 or #2:

// #3
template <std::convertible_to<std::string_view> T>
void foo(const T& msg);

// #4
template <typename T>
void foo(const T& msg) requires std::convertible_to<T, std::string_view>;

With respect to enable_if, there are three options:

// #5, non-type template parameter with default value ("version 1")
template <typename T, typename std::enable_if_t<std::is_convertible_v<T, std::string_view>, int> = 0>
void foo(const T& msg);

// #6, enable_if in the return type
template<typename T>
auto foo(const T& msg) -> typename std::enable_if_t<std::is_convertible_v<T, std::string_view>>;

// #7, defaulted template parameter ("version 2")
template<class T, typename = typename std::enable_if_t<std::is_convertible_v<T, std::string_view>>>
void foo(const T& msg);

Option #7 ("version 2") is rarely advisable, because default template parameters do not participate in the function signature. So, once you have two overloads, it is ambiguous. And overload sets grow.

Option #6 is not available for constructors, which lack a return type. But, in #6, you can name the function parameters which can be handy.

Option #5 is the most general SFINAE option. Prefer it, if you must SFINAE.

Regarding question #2, the relaxation on typename came in C++20, and is described here and here

Whats is type* in the expression std::enable_if

It's a pointer to a type exposed by std::enable_if if std::is_trivially_destructible<T>::value == true else it doesn't exist. The default type for it to expose is void.

Remember, with SFINAE we're only trying to trigger a substitution error, we can do this by trying to use the typedef type of std::enable_if. If std::is_trivially_destructible<T>::value is false then type won't exist and the function will be skipped for overload resolution.

We could also specify our own type, maybe that makes it clear:

std::enable_if<true, int>::type* intPointer;

Here, intPointer will be of type int*.


Without the checks of enable_if it'd look a bit like:

template <typename T>
struct enable_always
{
typedef T type;
};

Correct way to use std::enable_if

Note that enable_if is aimed to trigger SFINAE: if a template parameter substitution fails in its immediate context, it is not a compilation error.

This is exaclty what happens in class A: when the user calls a.method(...), the compiler attempts to instantiate member function template method, subsituting NN parameter with a constant, which might fail.

However, in case of B::method the "bad" substitution occurs during class template B instantiation, when the compiler substitues N. The failure occurs far from the parameter's immediate context, which is in this case template<typename T, std::size_t N> class B.

That's why in the second case you will get a compilation error, rather than SFINAE.

So, to enable/disable member functions depending on the class template parameter, use the first approach, combining conditions as you wish. For instance:

template <typename T, std::size_t N>
class A {
template <std::size_t NN = N, typename = std::enable_if_t<NN == 2 || NN == 0>>
void method(int, int)
{}

template <std::size_t NN = N, typename = std::enable_if_t<NN == 1 || NN == 0>>
void method(int)
{}
};

UPDATE: How enable_if works. Roughly, one can implement it like this:

template<bool, class T = void>
struct enable_if {};

template<class T>
struct enable_if<true, T> {
using type = T;
};

Note that if the first argument is false, enable_if doesn't have inner type, so enable_if<false, int>::type would be ill-formed. That's what triggers SFINAE.



Related Topics



Leave a reply



Submit