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
Should a Move Constructor Take a Const or Non-Const Rvalue Reference
How to Use an Array as Map Value
Opencv Cv::Mat and Eigen::Matrix
Templated Class Specialization Where Template Argument Is a Template
Function in C++ Returns by Value or by Reference
Linking to Msvc Dll from Mingw
Checking If an Iterator Is Valid
Does "Std::Size_T" Make Sense in C++
What Is the Underlying Type of a C++ Enum
How to Initialize 3D Array in C++
How to Hide Strings in a Exe or a Dll
How to Create My Own Ostream/Streambuf
Automatically Adding Enter/Exit Function Logs to a Project
The Implementation of Std::Forward
How Does _Builtin_Clear_Cache Work