180

Scott Meyers posted content and status of his next book EC++11. He wrote that one item in the book could be "Avoid std::enable_if in function signatures".

std::enable_if can be used as a function argument, as a return type or as a class template or function template parameter to conditionally remove functions or classes from overload resolution.

In this question all three solution are shown.

As function parameter:

template<typename T>
struct Check1
{
   template<typename U = T>
   U read(typename std::enable_if<
          std::is_same<U, int>::value >::type* = 0) { return 42; }

   template<typename U = T>
   U read(typename std::enable_if<
          std::is_same<U, double>::value >::type* = 0) { return 3.14; }   
};

As template parameter:

template<typename T>
struct Check2
{
   template<typename U = T, typename std::enable_if<
            std::is_same<U, int>::value, int>::type = 0>
   U read() { return 42; }

   template<typename U = T, typename std::enable_if<
            std::is_same<U, double>::value, int>::type = 0>
   U read() { return 3.14; }   
};

As return type:

template<typename T>
struct Check3
{
   template<typename U = T>
   typename std::enable_if<std::is_same<U, int>::value, U>::type read() {
      return 42;
   }

   template<typename U = T>
   typename std::enable_if<std::is_same<U, double>::value, U>::type read() {
      return 3.14;
   }   
};
  • Which solution should be preferred and why should I avoid others?
  • In which cases "Avoid std::enable_if in function signatures" concerns usage as return type (which is not part of normal function signature but of template specializations)?
  • Are there any differences for member and non-member function templates?
Jarod42
  • 203,559
  • 14
  • 181
  • 302
hansmaad
  • 18,417
  • 9
  • 53
  • 94
  • Because overloading is just as nice, usually. If anything, delegate to an implementation that uses (specialized) class templates. – sehe Jan 30 '13 at 09:09
  • Member functions differ in that the overload set includes overloads declared _after_ the current overload. This is particularly important when doing variadics delayed return type (where the return type is to be inferred from another overload) – sehe Jan 30 '13 at 09:11
  • 1
    Well, merely subjectively I have to say that while often being quite useful I don't like `std::enable_if` to clutter my function signatures (especially the ugly additional `nullptr` function argument version) because it always looks like what it is, a strange hack (for something a `static if` might do much more beautiful and clean) using template black-magic to exploit an interresting language feature. This is why I prefer tag-dispatching whenever possible (well, you still have additional strange arguments, but not in the public interface and also much less *ugly and cryptic*). – Christian Rau Jan 30 '13 at 10:41
  • 2
    I want to ask what does `=0` in `typename std::enable_if::value, int>::type = 0` accomplish? I couldn't find correct resources to understand it. I know the first part before `=0` has a member type `int` if `U` and `int` is the same. Many thanks! – astroboylrx Apr 24 '16 at 22:45
  • 5
    @astroboylrx Funny, I was just going to put a comment noting this. Basically, that =0 indicates that this is a defaulted, *non-type* template parameter. It's done this way because defaulted type template parameters are not part of the signature, so you cannot overload on them. – Nir Friedman Apr 27 '16 at 21:16
  • 1
    Upvoted the question since it has all the ways to use enable_if ! (; – puio Jan 04 '21 at 20:40

4 Answers4

120

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.

R. Martinho Fernandes
  • 228,013
  • 71
  • 433
  • 510
  • 4
    Using the `FUNCTION_REQUIRES` macro [here](http://stackoverflow.com/a/9220563/375343), makes it much nicer to read, and it works in C++03 compilers as well, and it relies on using `enable_if` in the return type. Also, using `enable_if` in function template parameters causes problems for overloading, because now the function signature aren't unique causing ambiguous overloading errors. – Paul Fultz II Mar 03 '13 at 17:05
  • 5
    This is an old question, but for anyone still reading: the solution to the issue raised by @Paul is to use `enable_if` with a defaulted non-type template parameter, which allows overloading. I.e. `enable_if_t = 0` instead of `typename = enable_if_t`. – Nir Friedman Apr 27 '16 at 21:17
  • wayback link to almost-static-if: https://web.archive.org/web/20150726012736/http://flamingdangerzone.com/cxx11/almost-static-if/ – davidbak May 09 '19 at 18:09
  • @R.MartinhoFernandes the `flamingdangerzone` link in your comment seems to lead to a spyware-installing page now. I flagged it for moderator attention. – nispio May 13 '20 at 23:33
58

std::enable_if relies on the "Substition Failure Is Not An Error" (aka SFINAE) principle during template argument deduction. This is a very fragile language feature and you need to be very careful to get it right.

  1. if your condition inside the enable_if contains a nested template or type definition (hint: look for :: tokens), then the resolution of these nested tempatles or types are usually a non-deduced context. Any substitution failure on such a non-deduced context is an error.
  2. the various conditions in multiple enable_if overloads cannot have any overlap because overload resolution would be ambiguous. This is something that you as an author need to check yourself, although you'd get good compiler warnings.
  3. enable_if manipulates the set of viable functions during overload resolution which can have surprising interactions depending on the presence of other functions that are brought in from other scopes (e.g. through ADL). This makes it not very robust.

In short, when it works it works, but when it doesn't it can be very hard to debug. A very good alternative is to use tag dispatching, i.e. to delegate to an implementation function (usually in a detail namespace or in a helper class) that receives a dummy argument based on the same compile-time condition that you use in the enable_if.

template<typename T>
T fun(T arg) 
{ 
    return detail::fun(arg, typename some_template_trait<T>::type() ); 
}

namespace detail {
    template<typename T>
    fun(T arg, std::false_type /* dummy */) { }

    template<typename T>
    fun(T arg, std::true_type /* dummy */) {}
}

Tag dispatching does not manipulate the overload set, but helps you select exactly the function you want by providing the proper arguments through a compile-time expression (e.g. in a type trait). In my experience, this is much easier to debug and get right. If you are an aspiring library writer of sophisticated type traits, you might need enable_if somehow, but for most regular use of compile-time conditions it's not recommended.

TemplateRex
  • 69,038
  • 19
  • 164
  • 304
  • 25
    Tag dispatching has one disadvantage though: if you have some trait that detects the presence of a function, and that function is implemented with the tag dispatching approach, it always report that member as present, and result in an error instead of a potential substitution failure. SFINAE is primarily a technique for removing overloads from candidate sets, and tag dispatching is a technique for selecting between two (or more) overloads. There is some overlap in functionality, but they are not equivalent. – R. Martinho Fernandes Jan 30 '13 at 09:40
  • @R.MartinhoFernandes can you give a short example, and illustrate how `enable_if` would get it right? – TemplateRex Jan 30 '13 at 09:44
  • @R.MartinhoFernandes I updated my answer to reflect your comment a bit, tnx. – TemplateRex Jan 30 '13 at 09:50
  • Short? Sorry :( Imagine you implement a function f as a template for all types in that fulfill some condition and not for other types. If you use tag dispatching, you won't have a false_type overload. That's fine, it causes an error when you try to use that op>> for the wrong types. You don't even need tag dispatching for this, you could just use static_assert and give better error messages. (to be continued) – R. Martinho Fernandes Jan 30 '13 at 10:03
  • But now consider a trait `is_f_able` that tells you if the expression `f(std::declval())` is valid. That trait will likely use some form `decltype` on that expression to detect this (the usual "has member" kind of trait). Problem is, `decltype(f(std::declval()))` will either "work" (i.e. not fail substitution) or produce a static assertion failure. Neither of these options is desirable because it means you never get a `::value` false from that trait. SFINAE makes the function *not be there*, and the trait detects that correctly to produce a false. – R. Martinho Fernandes Jan 30 '13 at 10:04
  • I hope that's clear :/ I can write this scenario as a code sample if it isn't clear. – R. Martinho Fernandes Jan 30 '13 at 10:07
  • 1
    @R.MartinhoFernandes I think a separate answer explaining these points might add value to the OP. :-) BTW, writing traits like `is_f_able` is something that I consider a task for library writers who can of course use SFINAE when that gives them an advantage, but for "regular" users and given a trait `is_f_able`, I think tag dispatching is easier. – TemplateRex Jan 30 '13 at 10:09
  • Good point! The fact that it did not fit in a single comment should have reminded me of that ;) I shall do so. – R. Martinho Fernandes Jan 30 '13 at 10:10
  • As a concrete example of this problem, `operator<` on a `std::vector` like container -- it could either exist or not based on SFINAE of `T` `<`, or it could exist and only work if `<` works on`T`. I have had to specialize my 'can be ordered' template for `vector` to do SFINAE because `vector` did not. – Yakk - Adam Nevraumont Jan 30 '13 at 10:24
  • @rhalbersma I am still writing my answer, and that makes me think more deeply and carefully about the issues: I am not sure that SFINAE is mostly for library writers. There is a conflict between some conveniences and some other conveniences here, and you cannot have your convenience and eat it too :'( – R. Martinho Fernandes Jan 30 '13 at 12:43
  • @R.MartinhoFernandes looking forward to the answer even more! BTW, Scott Meyers explains the always-prefer-consider-avoid-never qualifiers of his Items [here](http://scottmeyers.blogspot.nl/2013/01/effective-effective-books.html). So would you say "avoid" is appropriate or should it be "consider" or "beware of" `std::enable_if` ? – TemplateRex Jan 30 '13 at 12:49
  • +1. Btw there is an interesting [talk by Scott Meyers](http://scottmeyers.blogspot.it/2012/10/video-for-adventures-in-perfect.html) about perfect forwarding where he covers point 2. Here are the [slides from that talk](http://scottmeyers.blogspot.it/2012/10/video-for-adventures-in-perfect.html), the relevant section starting from slide 8. – Andy Prowl Jan 30 '13 at 23:33
  • @rhalbersma Thank you for your answer, although it doesn't respond to the question about usage in function signatures but criticize enable_if in general. – hansmaad Jan 31 '13 at 07:00
  • @R.MartinhoFernandes Will there be a separate answer from you, I should wait for? – hansmaad Jan 31 '13 at 07:02
  • 1
    @hansmaad I posted a short answer addressing your question, and will address the issue of "to SFINAE or to not SFINAE" in a blog post instead (it is a bit off-topic on this question). As soon as I get time to finish it, I mean. – R. Martinho Fernandes Jan 31 '13 at 10:43
  • 8
    SFINAE is "fragile"? What? – Lightness Races in Orbit Feb 08 '13 at 17:57
  • @LightnessRacesinOrbit relative compared tag dispatching, I'd call SFINAE "fragile". PErhaps "subtle" is a better word? – TemplateRex Feb 08 '13 at 18:33
  • "the various conditions in multiple enable_if overloads cannot have any overlap because of the One Definition Rule (ODR)" Sure, they'd be ambiguous at overload resolution, but what does the ODR has to do with this? – T.C. Sep 20 '15 at 04:16
  • Very good answer, thank you for sharing. All three points mentioned are often overlooked, specially point #1. – alecov Jul 19 '16 at 16:03
11

Which solution should be preferred and why should I avoid others?

Option 1: enable_if in the template parameter

  • It is usable in Constructors.

  • It is usable in user-defined conversion operator.

  • It requires C++11 or later.

  • In my opinion, it is the more readable (pre-C++20).

  • It is easy to misuse and produce errors with overloads:

    template<typename T, typename = std::enable_if_t<std::is_same<T, int>::value>>
    void f() {/*...*/}
    
    template<typename T, typename = std::enable_if_t<std::is_same<T, float>::value>>
    void f() {/*...*/} // Redefinition: both are just template<typename, typename> f()
    

    Notice the use of typename = std::enable_if_t<cond> instead of the correct std::enable_if_t<cond, int>::type = 0

Option 2 : enable_if in the return type

  • It cannot be used with constructors (which have no return type)
  • It cannot be used in user-defined conversion operator (as it is not deducible)
  • It can be used pre-C++11.
  • Second more readable IMO (pre-C++20).

Option 3: enable_if in a function parameter

  • It can be use pre-C++11.
  • It is usable in Constructors.
  • It cannot be used in user-defined conversion operators (they have no parameters)
  • It cannot be used in methods with a fixed number of arguments, such as unary/binary operators +, -, * and others.
  • It is safe for use in inheritance (see below).
  • Changes function signature (you have basically an extra as last argument void* = nullptr); this causes pointers pointers to the function to behave differently and so on.

Option4 (C++20) requires

There is now requires clauses

  • It is usable in Constructors

  • It is usable in user-defined conversion operator.

  • It requires C++20

  • IMO, the most readable

  • It is safe to use with inheritance (see below).

  • Can use directly template parameter of the class

    template <typename T>
    struct Check4
    {
       T read() requires(std::is_same<T, int>::value) { return 42; }
       T read() requires(std::is_same<T, double>::value) { return 3.14; }   
    };
    

Are there any differences for member and non-member function templates?

There are subtle differences with inheritance and using:

According to the using-declarator (emphasis mine):

namespace.udecl

The set of declarations introduced by the using-declarator is found by performing qualified name lookup ([basic.lookup.qual], [class.member.lookup]) for the name in the using-declarator, excluding functions that are hidden as described below.

...

When a using-declarator brings declarations from a base class into a derived class, member functions and member function templates in the derived class override and/or hide member functions and member function templates with the same name, parameter-type-list, cv-qualification, and ref-qualifier (if any) in a base class (rather than conflicting). Such hidden or overridden declarations are excluded from the set of declarations introduced by the using-declarator.

So for both template argument and return type, methods are hidden is following scenario:

struct Base
{
    template <std::size_t I, std::enable_if_t<I == 0>* = nullptr>
    void f() {}

    template <std::size_t I>
    std::enable_if_t<I == 0> g() {}
};

struct S : Base
{
    using Base::f; // Useless, f<0> is still hidden
    using Base::g; // Useless, g<0> is still hidden
    
    template <std::size_t I, std::enable_if_t<I == 1>* = nullptr>
    void f() {}

    template <std::size_t I>
    std::enable_if_t<I == 1> g() {}
};

Demo (gcc wrongly finds the base function).

Whereas with argument, similar scenario works:

struct Base
{
    template <std::size_t I>
    void h(std::enable_if_t<I == 0>* = nullptr) {}
};

struct S : Base
{
    using Base::h; // Base::h<0> is visible
    
    template <std::size_t I>
    void h(std::enable_if_t<I == 1>* = nullptr) {}
};

Demo

and with requires too:

struct Base
{
    template <std::size_t I>
    void f() requires(I == 0) { std::cout << "Base f 0\n";}
};

struct S : Base
{
    using Base::f;
    
    template <std::size_t I>
    void f() requires(I == 1) {}
};

Demo

Jarod42
  • 203,559
  • 14
  • 181
  • 302
1

"Which solution should be preferred and why should I avoid others?"

When the question was asked, std::enable_if from <type_traits> was the best tool available, and the other answers are reasonable up to C++17.

Nowadays in C++20 we have direct compiler support via requires.

#include <concepts
template<typename T>
struct Check20
{
   template<typename U = T>
   U read() requires std::same_as <U, int>
   { return 42; }
   
   template<typename U = T>
   U read() requires std::same_as <U, double>
   { return 3.14; }   
};
MSalters
  • 173,980
  • 10
  • 155
  • 350