c++language-lawyerc++20variadic-templatesc++-concepts

Expected behaviour when expanding a template parameter pack into an inner template declaration


There are two scenarios:

  1. You could use a template parameter pack and expand it into a nested template declaration of non-type template parameters:
template<class... Ts>
struct Outer {
    template<typename Ts::type...>
    void inner(){}
};  

I thought this would be pretty straight forward, since this is technically possible since C++11, but the resulting behavior differs greatly between compilers: Calling the following member functions on the variable Outer<std::type_identity<bool>, std::type_identity<int>> outer{}; results in the following (Demo - comment in/out the specific lines in main to see the compiler outputs):

outer.inner<true, 42>();:

outer.inner<1.0, 2.0>();:

outer.inner<true>();:

outer.inner<>();:

While clang behaves the way I would have personally expected, the discrepancy between compilers makes me think I'm doing something not officially allowed (maybe a clang extension?), so what is does the official standard say should happen here?

  1. With C++20 concepts it is now syntactically valid(?) to expand a template parameter pack into a nested template argument list using constrained type parameters.
template<class, std::size_t>
concept At = true;

template<std::size_t... Is>
struct Outer{
    template<At<Is>... Ts>
    void inner(Ts...){}
};

Calling the following member functions on the variable Outer<0, 1> outer{}; results in the following (Demo - comment in/out the specific lines in main to see the compiler outputs):

outer.inner(0, 1);:

outer.inner(0, 1, 2);:

outer.inner(0);:

Here my guess would be that GCC and MSVC are correct, as they seem to behave the same way as with the rewritten from, in which case clang also behaves like GCC and MSVC (Demo):

template<std::size_t... Is>
struct Outer{
    template<class... Ts>
        requires (At<Ts, Is> && ...)
    void inner(Ts...){}
};

But clang seems to create some sort of fixed length parameter pack which enables some interesting scenarios, like leading packs (Demo):

template<std::size_t... Is>
struct Outer{
    template<At<Is>... Ts, class U>
    void inner(Ts..., U){}
};

int main() {
   Outer outer<0, 1>{}.inner(true, 1, 2.0);
}

Which compiles with clang and deduces Ts = <bool, int> and U = double.

So the question is once again, which behavior is correct according to the standard?


Solution

  • For the first example, the behaviour is perfectly well-specified by the standard. According to [temp.param]/2

    [...] typename followed by a qualified-id denotes the type in a non-type parameter-declaration. [...]

    That means typename Ts::type means a non-type template parameter whose type is Ts::type. It can't really be anything else: it can't be a type template parameter named Ts::type because you can't declare a template parameter to have a qualified name.

    Then, [temp.param]/17 says

    [...] A template parameter pack that is a parameter-declaration whose type contains one or more unexpanded packs is a pack expansion. [...]

    One of the examples is

    template <class... T>
      struct value_holder {
        template <T... Values> struct apply { };    // Values is a non-type template parameter pack
      };                                            // and a pack expansion
    

    This makes it clear what is supposed to happen: the ... expands the pack T in the declaration of apply. For example, the class value_holder<bool, int> contains a nested class template apply that takes one bool and one int template argument.

    In your example the template parameter pack of the member template inner is unnamed, but this difference is not relevant: typename Ts::type... is just a parameter-declaration with the optional name omitted.

    Clang behaves correctly; I've no idea why the other compilers don't. Please file a bug against GCC.

    In your second example Clang is also correct. Later in [temp.param]/17 we have this sentence:

    A type parameter pack with a type-constraint that contains an unexpanded parameter pack is a pack expansion.

    At<Is>... Ts is a type parameter pack whose type-constraint is At<Is>, so this declaration expands the pack Is, resulting in a fixed-length type template parameter list. Ts denotes this fixed-length list, and is then expanded by the parameter declaration Ts... into a function parameter list whose length equals that of Is.