OoRoOrOoRoO
OoRoOrOoRoO

Reputation: 89

Not sure how enable_if is used and why it's important

After reading from a few websites, I think enable_if allows us to enable or restrict a type if a condition is true? I'm not quite sure, can someone clarify what it is exactly? I'm also not sure how it's used and in what scenarios it could be relevant within. I've also seen various ways its template parameters can be used, which further confused me. In how many ways can it be used?

For instance, does the following mean that the return type should be bool if the type T is an int?

typename std::enable_if<std::is_integral<T>::value,bool>::type
  is_odd (T i) {return bool(i%2);}

Upvotes: 1

Views: 1011

Answers (3)

Michael Chourdakis
Michael Chourdakis

Reputation: 11158

It allows to manipulate stuff at compile time using some constant. An example from one of my classes:

template <bool R>
class foo
{
 ...
    template <bool Q = R>
    typename std::enable_if<Q,void>::type
    Join()
    {
    }
 };

Join() is defined only if an object is instantiated with foo<true>. It's based on SFINAE. Compiler will not error when substituting, it will simply ignore the declaration.

Reference and examples here.

Upvotes: 2

Frodyne
Frodyne

Reputation: 3973

To understand this we have to dive into SFINAE or "Substitution Failure Is Not An Error". This is a somewhat hard to grasp principle that is also at the heart of a lot of compile-time template tricks.

Let us take a very simple example:

#include <iostream>

struct Bla {
    template <typename T,
        std::enable_if_t<std::is_integral<T>::value, int> = 0
    >
        static void klaf(T t)
    {
        std::cout << "int" << std::endl;
    }

    template <typename T,
        std::enable_if_t<std::is_floating_point<T>::value, int> = 0
    >
        static void klaf(T t)
    {
        std::cout << "float" << std::endl;
    }
};

int main()
{
    Bla::klaf(65);
    Bla::klaf(17.5);
}

Prints:

int
float

Now, how does this work? Well, in the case of Bla::klaf(65) the compiler finds two functions that match the name, then once name lookup is over, it tries to select the best one by substituting the types (IMPORTANT: Name lookup happens first and only once, then substitution.)

In substitution this happens (the second first, as it is more interesting):

template <typename T, std::enable_if_t<std::is_floating_point<T>::value, int> = 0>
  static void klaf(T t) {...}

-> T becomes int

template <int, std::enable_if_t<std::is_floating_point<int>::value, int> = 0>
  static void klaf(int t) {...}

-> is_floating_point<int>::value evaluates to false

template <int, std::enable_if_t<false, int> = 0>
  static void klaf(int t) {...}

-> enable_if_t<false,... evaluates to nothing

template <int, = 0>
  static void klaf(int t) {...}

-> the code is malformed: ", = 0" does not make sense.

In normal code this would be a compile error, but this is templates and "Substitution Failure Is Not An Error". In other words; the compiler is happy if something substitutes into valid code, forget all the stuff that doesn't.

And hey, the other Bla::klaf option does actually substitute into valid code:

template <typename T, std::enable_if_t<std::is_integral<T>::value, int> = 0>
  static void klaf(T t)

-> T becomes int

template <int, std::enable_if_t<std::is_integral<int>::value, int> = 0>
  static void klaf(int t)

-> is_integral<int>::value evaluates to true

template <int, std::enable_if_t<true, int> = 0>
  static void klaf(int t)

-> enable_if_t<true, int> evaluates to int

template <int, int = 0>
  static void klaf(int t)

-> This is actually valid code that the compiler can swallow.

Upvotes: 6

Klaus
Klaus

Reputation: 25603

enable_ifis only a small little helper which is used to implement SFINAE. https://en.cppreference.com/w/cpp/language/sfinae

In a very short: If a expansion of template arguments in the template declaration results in an error, the compiler will not stop or emit a error message nor a warning, the compiler will simply ignore the declaration and also the following definition.

enable_if will result in a error, if the condition is false.

One typical use case is something like that:

struct A{};
struct B{};

template<typename T>
struct Foo
{
    template<typename U = T>
        typename std::enable_if<std::is_same<U,A>::value>::type
        bar() { std::cout << "1" << std::endl; }

    template<typename U = T>
        typename std::enable_if<std::is_same<U,B>::value>::type
        bar() { std::cout << "2" << std::endl; }
};

int main()
{
    Foo<A>{}.bar();
    Foo<B>{}.bar();
}

Why we need SFINAE:

If you write generic code, you sometimes need to take some assumptions of the types you get into the template. Lets say, you expect you get a container type and you now want to implement iterating over it. So you must be able to generate iterators inside the templated function or method. But if you get some other type, this may not work, as the type has no default iterators for example. Now you simply can check with SFINAE, that your type is able to use iterators and you also can specialize a method for acing without such iterators. Only as an example!

SFINAE is quite a complex thing and error prone. Most common pitfall: Evaluation of template parameters in non deduced context!

Upvotes: 2

Related Questions