Reputation: 18905
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;
}
};
std::enable_if
in function signatures" concerns usage as return type (which is not part of normal function signature but of template specializations)?Upvotes: 183
Views: 41615
Reputation: 11
There is another option i thought is readable and simplest.
trailing return type
template <typename T>
auto GetName() -> std::enable_if_t<std::is_integral_v<T> || std::is_enum_v<T>, const char *>
{
return "Inter or enum";
}
ugly things just put behind function name
works like requires in cpp20
Upvotes: 0
Reputation: 217145
Which solution should be preferred and why should I avoid others?
enable_if
in the template parameterIt 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
enable_if
in the return typeenable_if
in a function parameter+
, -
, *
and others.void* = nullptr
); this causes pointers pointers to the function to behave differently and so on.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):
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) {}
};
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) {}
};
Upvotes: 13
Reputation: 179789
"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; }
};
Upvotes: 1
Reputation: 70516
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.
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.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.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.
Upvotes: 59
Reputation: 234414
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.
Upvotes: 122