SFINAE works differently in cases of type and non-

2019-01-09 11:44发布

问题:

Why does this code work:

template<
    typename T, 
    std::enable_if_t<std::is_same<T, int>::value, T>* = nullptr>
void Add(T) {}

template<
    typename T, 
    std::enable_if_t<!std::is_same<T, int>::value, T>* = nullptr>
void Add(T) {}

and can correctly distinguish between these two calls:

Add(1);
Add(1.0);

while the following code if compiled results in the redefinition of Add() error?

template<
    typename T, 
    typename = typename std::enable_if<std::is_same<T, int>::value, T>::type>
void Add(T) {}

template<
    typename T, 
    typename = typename std::enable_if<!std::is_same<T, int>::value, T>::type>
void Add(T) {}

So if the template parameter is type, then we have redefinition of the function, if it is non-type, then everything is ok.

回答1:

SFINAE is about substitution. So let us substitute!

template<
  typename T, 
  std::enable_if_t<std::is_same<T, int>::value, T>* = nullptr>
void Add(T) {}

template<
  typename T, 
  std::enable_if_t<!std::is_same<T, int>::value, T>* = nullptr>
void Add(T) {}

Becomes:

template<
  class T=int, 
  int* = nullptr>
void Add(int) {}

template<
  class T=int, 
  Substitution failure* = nullptr>
void Add(int) {

template<
  class T=double, 
  Substitution failure* = nullptr>
void Add(double) {}

template<
  class T=double
  double* = nullptr>
void Add(double) {}

Remove failures we get:

template<
  class T=int, 
  int* = nullptr>
void Add(int) {}
template<
  class T=double
  double* = nullptr>
void Add(double) {}

Now remove template parameter values:

template<
  class T, 
  int*>
void Add(T) {}
template<
  class T
  double*>
void Add(T) {}

These are different templates.

Now the one that messes up:

template<
  typename T, 
  typename = typename std::enable_if<std::is_same<T, int>::value, T>::type>
void Add(T) {}

template<
  typename T, 
  typename = typename std::enable_if<!std::is_same<T, int>::value, T>::type>
void Add(T) {}

Becomes:

template<
  typename T=int, 
  typename =int>
void Add(int) {}

template<
  typename int, 
  typename = Substitution failure >
void Add(int) {}

template<
  typename T=double, 
  typename = Substitution failure >
void Add(double) {}

template<
  typename T=double, 
  typename = double>
void Add(double) {}

Remove failures:

template<
  typename T=int, 
  typename =int>
void Add(int) {}
template<
  typename T=double, 
  typename = double>
void Add(double) {}

And now template parameter values:

template<
  typename T, 
  typename>
void Add(T) {}
template<
  typename T, 
  typename>
void Add(T) {}

These are the same template signature. And that is not allowed, error generated.

Why is there such a rule? Beyond the scope of this answer. I'm simply demonstrating how the two cases are different, and asserting that the standard treats them differently.

When you use a non-type template parameter like the above, you change the template signature not just the template parameter values. When you use a type template parameter like the above, you only change the template parameter values.



回答2:

Here, the problem is that the template signature for add() is the same: A function template taking two parameters type.

So when you write:

template<
    typename T, 
    typename = std::enable_if_t<std::is_same<T, int>::value, T>>
void Add(T) {}

That's fine, but when you write:

template<
    typename T, 
    typename = std::enable_if_t<!std::is_same<T, int>::value, T>>
void Add(T) {}

You are redefining the first add() template, only this time you specify a different default type for the second template parameter: in the end, you defined an overload for add() with the exact same signature, hence the error.

If you want to have several implementations like your question suggests, you should use std::enable_if_t as the return parameter of your template or use it in the same fashion as your first example. So your initial code becomes:

template<typename T>
std::enable_if_t<std::is_same<T, int>::value> Add(T) {}
template<typename T>
std::enable_if_t<!std::is_same<T, int>::value> Add(T) {}

Working example on Coliru

On the code above, if T == int, the second signature becomes invalid and that triggers SFINAE.

NB: Suppose you want N implementations. You can use the same trick as above but you'll need to make sure only one boolean among the N is true and the N-1 that remain are false, otherwise you'll get the exact same error!



回答3:

I think the problem is the fact that you can use a function even if a default template parameter does not compile by specifying a different value for it. Think of what would happen if you specified two template parameters in a call to add.



回答4:

I'll try to first give an example without the use of templates, but with default arguments. The following example is comparable to why the second example of yours fails, although it is not indicative of the inner workings of template overload resolution.

You have two functions declared as such:

void foo(int _arg1, int _arg2 = 3);

And

void foo(int _arg1, int _arg2 = 4);

Hopefully you realize that this will fail to compile, their is never going to be a way to distinguish between the two calls to foo with the default argument. It's completely ambiguous, how will the compiler ever know which default to choose? You may wonder why i used this example, after all shouldn't the template in the first example deduce different types? The short answer to that is no, and that's because the "signature" of the two templates in your second example:

template<
    typename T, 
    typename = typename std::enable_if<std::is_same<T, int>::value, T>::type>
void Add(T) {}

template<
    typename T, 
    typename = typename std::enable_if<!std::is_same<T, int>::value, T>::type>
void Add(T) {}

...have the exact same signature, which is:

template<typename T,typename>
void Add(T);

And (respectively)

template <typename T, typename>
void Add(T); 

Now you should start to understand the similarity's between the example i gave with non-templates and the example you provided that failed.



回答5:

SFINAE does not spread to default values for either types nor values. Only types of function arguments and result are used in this technique.