Joseph Larson
Joseph Larson

Reputation: 9058

Understanding more about type_traits

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.

Upvotes: 8

Views: 1159

Answers (1)

dfrib
dfrib

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

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.

Upvotes: 7

Related Questions