Clarify the ambiguity of partial template specialization

Viewed 81

I am confused by the error output of GCC for the partial specializations below.

// Primary
template<class T, class U1, class U2, class... Us>
struct S{};

// #1
template<class T, class... Us>
struct S<T, T, T, Us...>{};

// #2
template<class T, class U, class... Us>
struct S<T, T, U, Us...>{};

// #3
template<class T, class U, class... Us>
struct S<T, U, T, Us...>{};

// #4
template<class T, class U1, class U2, class U3, class... Us>
struct S<T, U1, U2, U3, Us...>{};

When I tried to call S<int, int, long, float, double> the compiler said it could not decide which of #2 and #4 to choose as the one instantiate. But I think #2 is more specialized than #4 regarding this, so it should make a decision.

My reasons are listed below:

When both #2 and #4 could be called, i.e the number of template arguments is no less than 4, from the way introduced here, we can define a fictitious function for #2 which is

template<class T, class U, class... Us>
f<X<T, T, U, Us...>); // #A

and one for #4

template<class T, class U1, class U2, class U3, class... Us>
f<X<T, U1, U2, U3, Us...>); // #B

Then, #A from #B (void(X<T, T, U, aUs...>) from void(X<U1, U2, U3, U4, bUs...>)):

  • P1=T, A1=U1: T=U1,
  • P2=T, A2=U2: T=U2: fails;

and #B from #A (void(X<T, U1, U2, U3, bUs...>) from void(X<U1, U2, U3, aUs...>)):

  • P1=T, A1=U1,
  • P2=U1, A2=U2,
  • P3=U2, A3=U3,
  • P4=U3, A4=<aUs...>[0] (<aUs...> is not empty in this case),
  • P5=Us..., A5=<aUs...>[1,] (<aUs...>[1,] may be empty now).

So #B from #A is successfully performed in this way, then I think #2 is more specialized than #4 so #2 should be chosen.

If GCC is right (most possible), I would like to know which part of my above statements goes wrong. Thanks very much.

A live example could be found here.

2 Answers

Conceptually neither #2 nor #4 is more specialized, because there are lists of template arguments for A such that #2 is viable, but not #4, as well as such sets for which #4 is viable, but #2 is not.

In your formal analysis everything looks correct, except that at

P4=U3, A4=<aUs...>[0] (<aUs...> is not empty in this case),

deduction fails, because you need to actually compare all of <aUs...> as a single (imagined) template argument A4 to be matched against P4 and per [temp.deduct.type]#9.2, because it originated from a pack expansion, A4 should then have either no corresponding element in the template argument list of the parameter or correspond to an element of the parameter's template argument list that also originated from a pack expansion, which P4 does not (since it is just U3).

For anyone who may meet the same problem.

The accepted answer referred to the WP and the rules underlying. In comments there are also straight-forward ones that clarify how #2 and #4 are incomparable w.r.t partial specialization.

If you have to write programs like this and get stuck here, read ahead.

(By saying a specialization responses, I intend to mean that it matches. (response should be respond)

As stated in the accepted answer and comment, for a given case S<int, int, long, float, double>, we have to choose which one of #2 and #4 to match. So we have to rewrite #2 (and #4) so that they do not conflict.

#1, #2, #3 only depends on the first three template arguments, and the class... Us easily makes them candidates for #4 mistakenly. So we remove it from their template header, i.e.

// #1
template<class T>
struct S<T, T, T> {};

// #2
template<class T, class U>
struct S<T, T, U> {};

// #3
template<class T, class U>
struct S<T, U, T> {};

After the rewriting, we have eliminated the conflict between #4 and the other 3, so the compiler would not complain about many candidates. For instantiation like S<int, int, long, float, double>, since it has 5 template arguments, #4 is the one to match.

But what if we would like #2 to match?

We have to rewrite #4 in the following way. #2 takes only 3 template arguments, so we could pass the <T, U1, U2> in #4's template header to #2 and make it derive from #2, i.e.

template <class T, class U1, class U2, class U3, class... Us>
struct S<T, U1, U2, U3, Us...>: public S<T, U1, U2> {}

In this way, for the given case, #2 would match. However, #4 will just degrades to a case which accepts 4 or more template arguments for #0~#3. i.e. S<int, long, float, double, int> now matches #0.

If that is not expected (we just want #0 to be a primary and work only when the number of template arguments is 3), to solve this we could use std::conditional, i.e.

template <class T, class U1, class U2, class U3, class... Us>
struct S<T, U1, U2, U3, Us...>: public std::conditional_t<bool(S<T, U1, U2>::value), S<T, U1, U2>, S4> {}

We have to make sure #0 has an static member value which evaluates to false and define another helper class S4 though.


A live example that checks whether the first template argument T is in the template list U1, U2, Us... could be found at godbolt. (It is inspired by the possible implementation of std::disjunction provided here)

Related