Reputation: 9058
I asked a question yesterday about template method overloading and resolving issues using type traits. I received some excellent answers, and they led me to a solution. And that solution led me to more reading.
I landed on a page at Fluent CPP -- https://www.fluentcpp.com/2018/05/18/make-sfinae-pretty-2-hidden-beauty-sfinae/ that was interesting, and then I listened to the Stephen Dewhurst talk that Mr. Boccara references. It was all fascinating.
I'm now trying to understand a little more. In the answers yesterday, I was given this solution:
template< class Function, class... Args,
std::enable_if_t<std::is_invocable_v<Function, Args...>, std::nullptr_t> = nullptr>
explicit MyClass( const std::string & theName, Function&& f, Args&&... args )
: name(theName)
{
runner(f, args...);
}
After reading the CPP Fluent post and watching the talk, I came to this final solution:
template< class Function, class... Args>
using IsInvocable = std::enable_if_t < std::is_invocable_v<Function, Args...> >;
template< class Function, class... Args, typename = IsInvocable<Function, Args...> >
explicit ThreadHandle( const std::string & name, Function && f, Args &&... args ) {
startWithName(name, f, args...);
}
The first bit just moves some of the syntax into a common include file, but overall, this is simpler. I think this is clean and requires little explanation, even for someone unfamiliar with using type traits.
What I'm wondering is this. All three answers I received used a more complex form of enable_if_t
like this:
std::enable_if_t<std::is_invocable_v<Function, Args...>, std::nullptr_t> = nullptr>
And I'm not sure why they would do that if I can do this instead:
std::enable_if_t< std::is_invocable_v < Function, Args... > >
Are there implications? Or is this simply a matter of the more complex one is C++11, and now C++ 14 and 17 allows a simpler form? Perhaps the people responding were simply helping me out by showing me the complete form.
To add to my confusion, one of the answers did this:
std::enable_if_t<!std::is_convertible_v<Function, std::string>, bool> = true>
And another one did this:
std::enable_if_t<std::is_invocable_v<Function, Args...>, int> = 0>
I don't really understand these implications, either.
Any help getting over the hurdle would be great. I imagine there will be cases I'll want the more complicated versions, so understanding it better would be good.
Upvotes: 8
Views: 1159
Reputation: 73176
Vocabulary
// template-head
template<typename T = T{}>
// ^^^^^^^^^^ ^^^- default template-argument
// \ type template-parameter
// template-head
template<int i = 0>
// ^^^^^ ^- default template-argument
// \ non-type template-parameter
Default template arguments are not part of a function template's type, meaning you cannot use the following approach:
// BAD: trying to define to SFINAE-mutually exclusive overloads.
template<typename T, typename = std::enable_if_t<some_predicate_v<T>>>
void f(T) {}
template<typename T, typename = std::enable_if_t<!some_predicate_v<T>>>
void f(T) {}
as these define the same function; see e.g.
for details.
Thus, the approach above is typically used when you do not do overloading of otherwise identical functions, whereas the other family is used when you need to differentiate overloads.
// Variation A.
template<typename T,
// non-type template parameter of type void*,
// defaulted to nullptr
std::enable_if_t<some_predicate_v<T>>* = nullptr>
void f(T) {}
// OK: not the same function.
template<typename T,
std::enable_if_t<!some_predicate_v<T>>* = nullptr>
void f(T) {}
// Variation B.
template<typename T,
// non-type template parameter of type bool,
// defaulted to true or false
std::enable_if_t<some_predicate_v<T>, bool> = true>
void f(T) {}
// OK: not the same function.
template<typename T,
std::enable_if_t<!some_predicate_v<T>, bool> = true>
void f(T) {}
// Variation C.
template<typename T,
// non-type template parameter of type int,
// defaulted to 0
std::enable_if_t<some_predicate_v<T>, int> = 0>
void f(T) {}
// OK not the same function.
template<typename T,
std::enable_if_t<!some_predicate_v<T>, int> = 0>
void f(T) {}
// Variation D (uncommon/noisy).
template<typename T,
// non-type template parameter of type std::nullptr_t,
// defaulted to nullptr
std::enable_if_t<some_predicate_v<T>, std::nullptr_t> = nullptr>
void f(T) {}
// OK: not the same function.
template<typename T,
std::enable_if_t<!some_predicate_v<T>, std::nullptr_t> = nullptr>
void f(T) {}
Note that for variation A we leverage the fact that the 2nd template parameter of std::enable_if
(alias via the _t
alias template) is defaulted to void
.
Upvotes: 7