Search code examples
c++templatesgccclangvariadic-templates

gcc and clang disagree on using alias templates as template template argument


The code below compiles using gcc 14.1.1 but clang 18.1.8 doesn't accept it. Which one is right and is there any workaround?

template <template <typename...> class T>
struct s {
    template <typename... U>
    using type = T<U...>;
};

/* the following definition of c and a are defined by the users of my library; they cannot be part of the workaround */
template <typename T>
class c {};

template <typename T>
using a = c<T>;
/* end of "user-defined" part */

int main()
{
    typename s<c>::type<int>{};
    typename s<a>::type<int>{}; // error, see bellow
}
❯ clang++ -Wall -std=c++23 -pedantic text.cpp -o test                                 
test.cpp:7:20: error: pack expansion used as argument for non-pack parameter of alias template
    7 |     using type = T<U...>;
      |                    ^~~~

Notes and related questions

  1. Making the alias template variadic makes it compile with clang but the template aliases are created by the users and the library doesn't have control over it (see code).

  2. The case described in Unpacking parameter packs in template aliases and Pack expansion for alias template fails with both GCC and clang. However, it could be that the root cause is the same and the provided solution uses a similar mechanism to the answer here. The code in this question works with GCC and only fails with clang. Perhaps it's due to using of template template arguments.

  3. The case described in Workaround for passing parameter pack to alias templates (which have non-pack parameters) is somewhat similar to this question (except for the template template argument) but the given answer does not apply to this one. As I described above, the aliases are defined in the user code and the library does not have control over it.


Solution

  • What the Standard says

    Which one is right [...]?

    Neither. Or both.

    At first read I thought that clang was wrong as: a class-template and an alias-template are synonyms1; they can both be used as template-arguments for template template-parameters interchangeably2.

    But commenters rightly pointed out that this does not prove anything. After more careful reading of the Standard, I only came to the conclusion that the norm does not answer this question properly. GCC is here applying what we developers expect, while clang takes a more conservative reading of the rules.

    This is, as pointed out by user1200257, CWG 1430:

    Originally, a pack expansion could not expand into a fixed-length template parameter list, but this was changed in N2555. This works fine for most templates, but causes issues with alias templates.


    Workaround

    [Is] there any workaround?

    Following the adage:

    Any problem can be solved by adding a level of indirection... except having too much levels of indirection.

    #include <functional>
    
    template<template <class...> class T, class... U>
    struct pack
    { using type = T<U...>; };
    
    template <template <typename...> class T>
    struct s
    {
        template <typename... U>
        using type = pack<T, U...>::type;
    };
    
    template <typename T>
    class c {};
    
    template <typename T>
    using a = c<T>;
    
    int main()
    {
        static_assert(std::is_same_v<c<int>, s<c>::type<int>>, "template-class");
        static_assert(std::is_same_v<c<int>, s<a>::type<int>>, "template-alias");
    }
    

    => Demo on Compiler Explorer <=


    1 [dcl.typedef]/1

    A name declared with the typedef specifier becomes a typedef-name. A typedef-name names the type associated with the identifier ([dcl.decl]) or simple-template-id ([temp.pre]); a typedef-name is thus a synonym for another type. A typedef-name does not introduce a new type the way a class declaration ([class.name]) or enum declaration ([dcl.enum]) does.

    2 [temp.arg]/1

    A template-argument for a template template-parameter shall be the name of a class template or an alias template, expressed as id-expression. Only primary templates are considered when matching the template template argument with the corresponding parameter; partial specializations are not considered even if their parameter lists match that of the template template parameter.