TartanLlama
TartanLlama

Reputation: 65770

Effects of dependent names on pack expansions as arguments for non-pack parameters of alias templates

This code produces a compiler error in GCC, Clang, and MSVC (this is a heavily reduced test case for some real code that actually does something useful):

#include <cstdint>
#include <concepts>

template <std::size_t N>
struct a {
    template <class T0, class...Ts>
    using f = T0;
};

template <std::size_t N>
struct b {
    template <class... Ts>
    using f = typename a<N>::template f<Ts...>;
};
      
static_assert(std::same_as<b<0>::f<int,char,bool>, int>);
int main() {
}

Error:

<source>:13:41: error: pack expansion used as argument for non-pack parameter of alias template
   13 |     using f = typename a<N>::template f<Ts...>;
      |                                         ^~~~~
<source>:12:5: note: in instantiation of template type alias 'f' requested here
   12 |     template <class... Ts>
      |     ^
<source>:16:28: note: in instantiation of template class 'b<0>' requested here
   16 | static_assert(std::same_as<b<0>::f<int,char,bool>, int>);
      |                            ^
<source>:6:17: note: template parameter is declared here
    6 | template <class T0, class...Ts>
      |                 ^

Godbolt link: https://godbolt.org/z/Thrvon7T4

If I change the instantiation of a to be dependent on Ts..., then it compiles cleanly:

template <std::size_t N>
struct b {
    template <class... Ts>
    using f = typename a<((void)sizeof...(Ts),N)>::template f<Ts...>;
};

Godbolt link: https://godbolt.org/z/sao7fzzYc

What are the relevant rules that govern this difference? In other words, why is the version where the specialization of a is dependent on Ts well-formed, but the version where it is not is ill-formed?


Here is a simpler way to reproduce:

template <typename T, typename... P>
using type1 = T;

template <typename... P>
using type2 = type1<P...>;

LIVE

error: pack expansion argument for non-pack parameter 'T' of alias template 'template<class T, class ... P> using type1 = T'
using type2 = type1<P...>;

NB AS shown in compiler explorer, the issue seems to be the first template being an aliased type, as with a plain class there's no such issue.

Upvotes: 7

Views: 132

Answers (1)

Oersted
Oersted

Reputation: 2776

This is only a partial answer. Noticeably, it does not address why using f = typename a<((void)sizeof...(Ts),N)>::template f<Ts...>; is working.

What can be observed is a difference in handling of alias template and other kind of templates:

#include <type_traits>

template <typename T, typename... P>
using type_alias1 = T;

// refuses to dispatch P on T and the parameter pack
// template <typename... P>
// using type_alias2 = type_alias1 <P...>;

template <typename T, typename... P>
struct wrapped_type_alias {
    using type = T;
};

// accepts to dispatch P on T and the parameter pack
template <typename... P>
using type_alias3 = wrapped_type_alias<P...>::type;

int main() {
    // static_assert(std::is_same_v<type_alias2<int, bool, char>, int>);
    static_assert(std::is_same_v<type_alias3<int, bool, char>, int>);
}

LIVE

When trying to pass a parameter pack expansion to an alias template; OP's error occurs. Passing it to another kind of template works as expected (the first element of the pack is associated to the non-pack parameter).

This behavior does not depend of where the pack expansion occurs (declaring a (member) variable of type type_alias1<P...> fails also).

Unfortunately, I'm enable to find a corresponding rule within the standard, though the 3 tested compilers (gcc, clang, cl) exhibit the same behavior.

A bit of research seems to indicate that this a problem known from a long time:

Bug report have been filled but it seems that it still an open discussion if it's a bug or not:


As a sidenote, workarounds exist:

template <typename T, typename... P>
using type_alias1 = T;

template <typename T, typename... P>
using type_alias2 = type_alias1<T,P...>;

LIVE

The template using the pack expansion mimics the original template parameters list.

namespace impl
{
template <class T0, class... TS>
struct first {
    using type = T0;
};
}
template <typename... P>
using type_alias1 = impl::first<P...>::type;

template <typename... P>
using type_alias2 = type_alias1<P...>;

LIVE

The original template alias is modified to accept only a template parameter pack that is forwarded to a class template, for which the expansion issue does not occur.

Upvotes: 2

Related Questions