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.
Type traits are a clever technique used in C++ template metaprogramming that gives you the ability to inspect and transform the properties of types.
Use the keyword typename if you have a qualified name that refers to a type and depends on a template parameter. Only use the keyword typename in template declarations and definitions.
In C++, we can think about type traits as properties of a type. The <type_traits> header was an addition introduced by C++11. Type traits can be used in template metaprogramming to inspect or even to modify the properties of a type.
Now let’s have a look at how type traits can alter types. There are templates shipped in the <type_traits> header that can add or remove const and/or volatile specifiers from a given type Let’s see three examples. With std::add_const / std::remove_const you can add/remove the topmost const of a type:
Many Type A people try to show dominance in business and personal interactions, disregarding the wishes and needs of others in favor of their own. Years of Type A behavior and stress can prompt physical characteristics and changes, including:
Type traits: Classes to obtain characteristics of types in the form of compile-time constant values. Type transformations: Classes to obtain new types by applying specific transformations to existing types. A basic trait for types is the categories in which they can be classified. This is a chart on how these categories overlap:
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
.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With