There are two scenarios:
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>();
:
error: wrong number of template arguments (2, should be 1)
outer.inner<1.0, 2.0>();
:
error: wrong number of template arguments (2, should be 1)
candidate template ignored: invalid explicitly-specified argument for template parameter 'Vals'
Failed to specialize function template 'void Outer<std::type_identity<bool>, std::type_identity<int>>::inner(void)'
(followed by an ICE
)outer.inner<true>();
:
ICE
candidate template ignored: deduced too few arguments for expanded pack 'Vals'; no argument for 2nd expanded parameter in deduced argument pack <true>
outer.inner<>();
:
candidate template ignored: deduced too few arguments for expanded pack 'Vals'; no argument for 1st expanded parameter in deduced argument pack <>
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?
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);
:
error: mismatched argument pack lengths while expanding 'At<Ts, Is>'
candidate function [with Ts = <int, int>] not viable: requires 2 arguments, but 3 were provided
the associated constraints are not satisfied
outer.inner(0);
:
error: mismatched argument pack lengths while expanding 'At<Ts, Is>'
candidate template ignored: deduced too few arguments for expanded pack 'Ts'; no argument for 2nd expanded parameter in deduced argument pack <int>
the associated constraints are not satisfied
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?
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
.