Sfinae Works Differently in Cases of Type and Non-Type Template Parameters

How does changing a template argument from a type to a non-type make SFINAE work?

Mainly because [temp.over.link]/6 does not talk about template default argument:

Two template-heads are equivalent if their template-parameter-lists have the same length, corresponding template-parameters are equivalent, and if either has a requires-clause, they both have requires-clauses and the corresponding constraint-expressions are equivalent. Two template-parameters are equivalent under the following conditions:

  • they declare template parameters of the same kind,

  • if either declares a template parameter pack, they both do,

  • if they declare non-type template parameters, they have equivalent types,

  • if they declare template template parameters, their template parameters are equivalent, and

  • if either is declared with a qualified-concept-name, they both are, and the qualified-concept-names are equivalent.

Then by [temp.over.link]/7:

Two function templates are equivalent if they are declared in the same scope, have the same name, have equivalent template-heads, and have return types, parameter lists, and trailing requires-clauses (if any) that are equivalent using the rules described above to compare expressions involving template parameters.

... the two templates in your first example are equivalent, while the two templates in your second example are not. So the two templates in your first example declare the same entity and result in an ill-formed construct by [class.mem]/5:

A member shall not be declared twice in the member-specification, ...

SFINAE working in return type but not as template parameter

You should take a look at 14.5.6.1 Function template overloading (C++11 standard) where function templates equivalency is defined. In short, default template arguments are not considered, so in the 1st case you have the same function template defined twice. In the 2nd case you have expression referring template parameters used in the return type (again see 14.5.6.1/4). Since this expression is part of signature you get two different function template declarations and thus SFINAE get a chance to work.

What is wrong with my application of SFINAE when trying to implement a type trait?

SFINAE applied to class templates is primarily about choosing between partial specialisations of the template. The problem in your snippet is that there are no partial specialisations in sight. You define two primary class templates, with the same template name. That's a redefinition error.

To make it work, we should restructure the relationship between the trait implementations in such as way that they specialise the same template.

namespace detail {
template <typename T, typename = void> // Non specialised case
struct BaseType {
using Type = std::decay_t<T>;
};

template <typename T>
struct BaseType<T, std::enable_if_t<std::is_enum_v<T>>> {
using Type = std::underlying_type_t<T>;
};
}

template <class T>
using base_type_t = typename detail::BaseType<T>::Type;

The specialisation provides a void type for the second argument (just like the primary would be instantiated with). But because it does so in a "special way", partial ordering considers it more specialised. When substitution fails (we don't pass an enum), the primary becomes the fallback.

You can provide as many such specialisation as you want, so long as the second template argument is always void, and all specialisations have mutually exclusive conditions.

Issue with multiple overloads using enable_if

From cppreference, which has this very example as a Note:

A common mistake is to declare two function templates that differ only in their default template arguments. This does not work because the declarations are treated as redeclarations of the same function template (default template arguments are not accounted for in function template equivalence).

So what you need to do is not make the sfinae'd type a default argument. Instead, you could make it resolve to some type, e.g. int, and give it a default value, like this:

template <typename T, enable_if_t<is_same_v<T, int>, int> = 0>
void qw(T t)
{
std::cout << "int " << endl;
}

template <typename T, enable_if_t<is_same_v<T, float>, int> = 0>
void qw(T t)
{
cout << "float" << endl;
}

SFINAE works as return type, but not parameter type

std::enable_if<..., T>::type is a nested name, and as such cannot be deduced:

See [temp.deduct.type]/5:

The non-deduced contexts are:


The nested-name-specifier of a type that was specified using a qualified-id.

. . .

As a workaround, move the enable_if to a separate template argument:

template <typename T, size_t N, size_t M, typename std::enable_if<(M == 1 || N == 1), int>::type = 0>
static inline T Length(
const Matrix<T, N, M> & input)

SFINAE works with deduction but fails with substitution

Deduction of the function called in a function call expression is performed in two steps:

  1. Determination of the set of viable functions;
  2. Determination of the best viable function.

The set of viable function can only contain function declaration and template function specialization declaration.

So when a call expression (test(a,b) or test<A>(a,b)) names a template function, it is necessary to determine all template arguments: this is called template argument deduction. This is performed in three steps [temp.deduct]:

  1. Subsitution of explicitly provided template arguments (in names<A>(x,y) A is explicitly provided);(substitution means that in the function template delcaration, the template parameter are replaced by their argument)
  2. Deduction of template arguments that are not provided;
  3. Substitution of deduced template argument.

Call expression test(a,b)

  1. There are no explictly provided template argument.
  2. T is deduced to A for the first template function, deduction fails for the second template function [temp.deduct.type]/8. So the second template function will not participate to overload resolution
  3. A is subsituted in the declaration of the first template function. The subsitution succeeds.

So there is only one overload in the set and it is selected by overload resolution.

Call expression test<A>(a,b)

(Edit after the pertinent remarks of @T.C. and @geza)

  1. The template argument is provided: A and it is substituted in the declaration of the two template functions. This substitution only involves the instantiation of the declaration of the function template specialization. So it is fine for the two template
  2. No deduction of template argument
  3. No substitution of deduced template argument.

So the two template specializations, test<A>(A,A) and test<A>(Wrapper<A>,Wrapper<A>), participate in overload resolution. First the compiler must determine which function are viable. To do that the compiler needs to find an implicit conversion sequence that converts the function argument to the function parameter type [over.match.viable]/4:

Third, for F to be a viable function, there shall exist for each argument an implicit conversion sequence that converts that argument to the corresponding parameter of F.

For the second overload, in order to find a conversion to Wrapper<A> the compiler needs the definition of this class. So it (implicitly) instantiates it. This is this instantiation that causes the observed error generated by compilers.



Related Topics



Leave a reply



Submit