Why the default value is needed for `std::enable_if`?
Asked Answered
N

2

5

Why I have to use the default value (::type = 0) in this std::enable_if usage?

I see examples, where it works without it. for example https://foonathan.net/blog/2015/11/30/overload-resolution-4.html

#include<iostream>
#include<type_traits>

template <typename T,
          typename std::enable_if<std::is_integral<T>::value, T>::type = 0>
void do_stuff(T t) {
    std::cout << "do_stuff integral\n";
}

template <typename T,
          typename std::enable_if<std::is_class<T>::value, T>::type = 0>
void do_stuff(T t) {
    std::cout << "do_stuff class\n";
}

int main()
{
    do_stuff(32);
    return 0;
}

I got error message:

temp.cpp:6:6: note:   template argument deduction/substitution failed:
temp.cpp:18:13: note:   couldn't deduce template parameter ‘<anonymous>’

It should be deduced to

template <template T, int>
void do_stuff(T t)

which is a valid code. What I'm doing wrong? (gcc version 7.4.0)

Nationalist answered 19/5, 2019 at 15:56 Comment(8)
In the example you cite, the invocation of enable_if is the default argument. It looks like this: typename = std::enable_if<...>::type. It's the template type parameter (with its name omitted) given a default value. In your code, you have a non-type template parameter of type std::enable_if<...>::type, with no way to deduce it.Dougie
@IgorTandetnik When I do this I get: temp.cpp:13:6: error: redefinition of ‘template<class T, class> void do_stuff(T)’ void do_stuff(T t) { ^~~~~~~~ temp.cpp:7:6: note: ‘template<class T, class> void do_stuff(T)’ previously declared here void do_stuff(T t) {Nationalist
The example in the article doesn't compile either. Probably time to look for a better article.Dougie
@IgorTandetnik. It's Jonathan Müller's post (twitter.com/foonathan). It should work :)Nationalist
Well, maybe it should, but it does notDougie
@DanielLangr. Yes, this is the working example, I'm asking why ::type = 0 has to be there? Try to remove it.Nationalist
@Nationalist I see, misinterpreted your question (it would be better to post a code that produces the error). Anyway, AnT's answer explains the problem.Anent
In the second overload for class-type T, note the = 0 default template argument won't compile if class T can't be copy-initialized from a 0. This is why you'll see some code use instead template <..., typename std::enable_if< expr >::type* = nullptr>. In that usage, the type resulting from enable_if is void if expr is true, so the last template parameter type is always void* no matter what the other template parameter(s) are.Behistun
A
5

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.

Agone answered 19/5, 2019 at 16:31 Comment(6)
it seems that you are right. I'm still missing there something. My understanding is that if compiler deduces the first T to int, then also the second T is int, so -> x is int. What do you mean by "arguments for x?", type ? Thx for clarificationNationalist
@MiCha: You seem to be missing the fact that in both cases the first template parameter is a type parameter, while the second is a non-type parameter (a "value" parameter). In my example parameter T stands for int, but x does not stand for int. x is a value parameter, it stands for some value of type int. What value is the compiler supposed to use? The compiler does not know.Agone
@ AnT Yes, you are right, but that's the difference between your and my example. In my case also the second template parameter is type (keyword typename) and the enable_if::type is alias for T. So if T is int then ::type is int, so why the type parameter needs also the default value? ThxNationalist
@MiCha: No, no, no, no, no. Your example is exactly the same as mine. You simply misinterpreted the meaning of the word typename in the second parameter. No, it does not mean that this is a type parameter. Your second parameter is a value parameter, just like mine. See the updated answer.Agone
That's it! Thank you !Nationalist
@Nationalist As an aside, avoid using a dummy-parameter when you can use the return-type, to avoid pushing it into the decorated name, in case that ends up in a symbole-table.Iloilo
A
1

I cannot reproduce your error; anyway I get an error calling do_stuff() with a class. By example

do_stuff(std::string{"abc"})

This is because do_stuff() become do_stuff<std::string, std::string = 0>() and a template value can't be of type std::string (and not with default value zero).

Suggestion: rewrite your functions imposing int as type for the value in second position

template <typename T, // .....................................VVV  int, not T
          typename std::enable_if<std::is_integral<T>::value, int>::type = 0>
void do_stuff(T t) {
    std::cout << "do_stuff integral\n";
}

template <typename T, // ..................................VVV  int, not T
          typename std::enable_if<std::is_class<T>::value, int>::type = 0>
void do_stuff(T t) {
    std::cout << "do_stuff class\n";
}

This way, calling do_stuff(std::string{"abc"}), you enable do_stuff<std::string, int = 0>() that is acceptable.

Articulation answered 19/5, 2019 at 16:5 Comment(3)
Yes, the best is: template <typename T, typename std::enable_if<std::is_integral<T>::value, T>::type* = nullptr>. The question is, why this nullptr have to be there ?Nationalist
@Nationalist Well, where else is the value of this non-type template parameter supposed to come from? You basically have template <typename T, void* x> void do_stuff(T); - the compiler needs the value for xDougie
@Nationalist - There is no needs of use T (directly or as a pointer); int = 0 works perfectly. Anyway, the = nullptr (or = 0, in my example) is necessary to avoid to explicit it; otherwise you have to call do_stuff<int, nullptr>(42) instead of do_stuff(42);.Articulation

© 2022 - 2024 — McMap. All rights reserved.