Understanding more about type_traits
Asked Answered
B

1

8

Setup

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...);
     }

Alternative Answer

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.

The Question

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.

Bootery answered 14/12, 2021 at 14:32 Comment(3)
std::enable_if_t< std::is_invocable_v < Function, Args... > > is void when the condition is true. That's fine for using IsInvocable = void; but doesn't work for template parameters. In that case you instead make it void* or some other simple type and assign a default value so the user doesn't have to provide anything. I'm not sure this is the specific reason that applies here, but it is the first that comes to mind.Lancers
There are a number of answers on enable_if and SFINAE on Stack overflow: do any of these answer your question? Q/A 1, Q/A 2. Furthermore cppreference also has some information.Cist
Here's the biggest why https://mcmap.net/q/563316/-sfinae-works-differently-in-cases-of-type-and-non-type-template-parameters/817643Pieter
P
7

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

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.

... whereas different types of a non-type template parameters can be used as an alternative

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.

Paroxysm answered 14/12, 2021 at 15:32 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.