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
std::enable_if
to clutter my function signatures (especially the ugly additionalnullptr
function argument version) because it always looks like what it is, a strange hack (for something astatic 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). – Jehoash=0
intypename std::enable_if<std::is_same<U, int>::value, int>::type = 0
accomplish? I couldn't find correct resources to understand it. I know the first part before=0
has a member typeint
ifU
andint
is the same. Many thanks! – Mendelssohn