7

From clang 9.0.0 onwards, all versions of clang reject the following code when compiled with -std=c++17 (or -std=c++20):

#include <utility>

template<class L, std::size_t N>
struct A;

// #1
template
    < template<class X, X...> class Holder
    , class T, T... Cst
    , std::size_t N
    >
struct A<Holder<T, Cst...>, N>;

// #2
template
    < template<class X, X...> class Holder
    , class T, T... Cst
    >
struct A<Holder<T, Cst...>, 0> {};

int main() {
    A<std::index_sequence<>, 0> a{}; // supposed to select #2
}

Supposedly because instantiating A<std::index_sequence<>, 0> is ambiguous:

<source>:20:33: error: ambiguous partial specializations of 'A<std::integer_sequence<unsigned long>, 0>'
    A<std::index_sequence<>, 0> a{};
                                ^
<source>:11:8: note: partial specialization matches [with Holder = integer_sequence, T = unsigned long, Cst = <>, N = 0]
struct A<Holder<T, Cst...>, N>;
       ^
<source>:17:8: note: partial specialization matches [with Holder = integer_sequence, T = unsigned long, Cst = <>]
struct A<Holder<T, Cst...>, 0> {};

Compiling with -std=c++14 doesn't exhibit the error. Compiling with gcc or clang 8.0.1 (or earlier) doesn't either. godbolt link

I don't understand why that would be ambiguous. I'd expect the following to correctly deduce Holder = Holder0, T=T0, Cst=Cst0, N=2 when calling f:

template
    < template<class X, X...> class Holder
    , class T, T... Cst
    , std::size_t N
    >
void f(A<Holder<T, Cst...>, N>) {} // obtained from #1

template
    < template<class X, X...> class Holder0
    , class T0, T0... Cst0
    >
struct Context {
    static void g() {
        f(A<Holder0<T0, Cst0...>, 0>{}); // obtained from #2
    }
};

godbolt link
whereas the converse would usually fail for an arbitrary N:

template
    < template<class X, X...> class Holder
    , class T, T... Cst
    >
void f(A<Holder<T, Cst...>, 0>) {} // obtained from #2

template
    < template<class X, X...> class Holder0
    , class T0
    , std::size_t N
    , T0... Cst0
    >
struct Context {
    static void g() {
        f(A<Holder0<T0, Cst0...>, N>{}); // obtained from #1
    }
};

godbolt link
From what I understand of N4659 (final working draft of c++17) and [temp.class.order]/1, that should imply that #2 is more specialized than #1.

Then again, I'm not good at reading the standard, and clang only giving me incorrect results with recent versions (both of the compiler and of the c++ standard) makes me second-guess myself.

So, is clang right or wrong here? If it's right, what am I missing?

Caninonos
  • 1,214
  • 7
  • 12
  • Somewhat unrelated, but is `N` supposed to be `sizeof...(Cst)`? – Nelfeal May 27 '23 at 10:13
  • @Nelfeal No. In the original code, `N <= sizeof...(Cst)` as N was the length of a subsequence of `Holder` (well, sort-of). Here's something a bit closer to the original code if you're curious: https://godbolt.org/z/oKPvedq4P (I can figure out a way to fix it but I was surprised it didn't work, hence the question). – Caninonos May 27 '23 at 11:26

1 Answers1

1

Clang is definitely wrong here. Your analysis is correct.

Consider the following snippet, similar to yours but without any parameter pack and with function templates instead of class template specializations (the rewrite explained in [temp.class.order]):

#include <utility>

template<class L, std::size_t N>
struct A {};

template<class T, T>
struct TestHolder {};

template<template<class X, X> class Holder, class T, T Cst, std::size_t N>
void f(A<Holder<T, Cst>, N>) {}

template<template<class X, X> class Holder, class T, T Cst>
void f(A<Holder<T, Cst>, 0>) {}

int main() {
    f(A<TestHolder<int, 0>, 0>{});
}

This exhibits the same issue (demo). Here are a few possible changes that should not affect the specialization ordering but that clang can compile.

  1. Specifying the argument Holder in both specializations (demo):
template<class T, T Cst, std::size_t N>
void f(A<TestHolder<T, Cst>, N>) {}

template<class T, T Cst>
void f(A<TestHolder<T, Cst>, 0>) {}
  1. Specifying the argument T in both specializations (demo):
template<template<class X, X> class Holder, int Cst, std::size_t N>
void f(A<Holder<int, Cst>, N>) {}

template<template<class X, X> class Holder, int Cst>
void f(A<Holder<int, Cst>, 0>) {}
  1. Specifying the argument Cst in both specializations (demo):
template<template<class X, X> class Holder, class T, std::size_t N>
void f(A<Holder<T, T{}>, N>) {}

template<template<class X, X> class Holder, class T>
void f(A<Holder<T, T{}>, 0>) {}

For some reason, MSVC chokes on this one when using 0 instead of T{}.

Also note that in your original code, replacing Holder with std::integer_sequence works as well:

#include <utility>

template<class, std::size_t N>
struct A;

template<class T, T... Cst, std::size_t N>
struct A<std::integer_sequence<T, Cst...>, N>;

template<class T, T... Cst>
struct A<std::integer_sequence<T, Cst...>, 0> {};

int main() {
    A<std::index_sequence<>, 0> a{};
}

Demo

Nelfeal
  • 12,593
  • 1
  • 20
  • 39