How do variadic type template parameters and non-type template parameters of the template template parameter of a nested class constrain each other?
Asked Answered
D

2

17

Consider the following structure: S is a class template with a variadic pack of template type parameters ...Ts. The class contains a nested class template N, which has a single template template parameter C. C itself is templated with a variadic pack of non-type template parameters that have exactly the types Ts....

template <typename ...Ts>
struct S 
{
    template <template <Ts...> typename C>
    struct N 
    {
        C<42> c;
    };
};

GCC rejects the declaration of c with:

error: expansion pattern '<anonymous>' contains no parameter packs
        C<42> c;
            ^

I don't know what this diagnostic means, and I'm guessing this is a GCC bug.


Clang accepts this structure, and also accepts a reasonable instantiation such as:

template<int> struct W {};
S<int>::N<W> w;             // ok, makes sense

Of course, the declaration C<42> c; itself places constraints on the template used as an argument for the parameter C, which will be required even if the Ts... in C were simply auto.... e.g. the first argument of Ts... must be a type that is implicitly convertible from an int. Also, the remaining parameters in Ts..., if any, must have defaults.

template<bool(*)()> struct X1 {};
S<int>::N<X1> x1;                  // error: value of type 'int' is not implicitly convertible to 'bool (*)()'
        
template<int, char> struct X2 {};
S<int>::N<X2> x2;                  // error: too few template arguments for class template 'X2'

Interestingly, there do appear to be constraints imposed by the relationship between ...Ts and Ts.... e.g. all the specified arguments for S must now be valid non-type template parameter types:

template<int> struct Y {};
S<int, void>::N<Y> y;       // error: a non-type template parameter cannot have type 'void' 

On the other hand, Clang also accepts instantiations with seemingly incompatible arguments for S, and non-type template parameters for C:

template<int> struct Z {};
S<bool(*)(), bool>::N<Z> z;  // ok ?? But why? both number and type of 'Ts' is different 

Here's a demo.

So what are the rules for how ...Ts and Ts... constrain each other in this structure?


I came across this issue while trying to understand this question where it's indicated that MSVC doesn't accept the code as well.

Dalhousie answered 26/11, 2020 at 3:12 Comment(9)
An interesting question is if the default arguments should apply to Z when named as C. If yes, then checking N is harder; if not, then gcc is right.Duvall
@Yakk-AdamNevraumont I'm not sure I understand why the default arguments are important. If the declaration in N is C<1> n; then b is accepted as well, and X has no default arguments.Dalhousie
Even without expected default, gcc complains Demo.Dearly
@Dalhousie if default arguments are not "passed through" then your template is only valid for an empty pack, which is illegal last I checked (ill-formed, ndr). C<1> case does not have that problem.Duvall
@Yakk-AdamNevraumont Oh, I didn't realize that. The empty pack case is not actually relevant to my question, I'll edit it.Dalhousie
@Dalhousie Also, parameter packs containing only the number 42 are also ill-formed, ndr -- that is known as the towel rule. (this is a joke, as conveying humour on the internet is difficult without being explicit)Duvall
@Yakk-AdamNevraumont Agreed. I would have gotten this one from the "towel rule" bit, good one btw :), but thanks for being explicit. It never hurts to be clear when there are templates involved ;)Dalhousie
nested classes in C++ are so frustratingly under specified IMOLeukemia
@Leukemia Indeed. I'm not even sure where in [temp] or [class] I should be looking :( If clang simply accepted all instantiations, I would assume IFNDR, but the fact that there seems to be some logic to what constraints are checked, I assume there's something meaningful going on. Which is now confirmed to some extent by the bug report you found.Dalhousie
L
4

For your primary question (nested class with non-type template arguments dependent on template arguments in outer class) this is a bug in GCC, #86883 (See comment #3)

Leukemia answered 30/11, 2020 at 16:38 Comment(5)
Thanks so much for tracking down the bug. It's not actually my primary question, as I mentioned that I assumed it's a gcc bug. My primary question is asking about how the constraints work. I see that the question title is misleading. I'll edit it.Dalhousie
Interestingly, the bug report is 2 years old, but it's still Unconfirmed.Dalhousie
@cigien: Yeah, I saw that. Seems to be related to other bugs along the same lines. All probably related to the changeset regarding handling lambdas in templates. Aside: I wanted a good place to say this, but not sure the answer is the best: I am guessing that allowing S<bool(*)(), bool>::N<Z> z; is a bug, but I cannot prove it yet. I'm guessing that MSVC and clang treat it as a redeclaration of the same template and then mistakenly allow the actual type to have different template args.Leukemia
@Dalhousie Heck, Clang allows S<bool, bool, bool, bool, unsigned long long, char, short, unsigned char, long long>::N<Z> z;, which doesn't seem right; per [temp.arg.template]Leukemia
Yeah, I think there are several rules the last snippet is violating. Clang probably hasn't implemented those checks yet. The interesting question is, what are the rules for what should be allowed? I'm finding the standard particularly impenetrable on this :(Dalhousie
M
3

I think Clang is not right.
temp.param#15

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

For the template template parameter of nested template class N, that is template <Ts...> typename C, where Ts... is a pack instantiation, which has the following rule:

Each Ei is generated by instantiating the pattern and replacing each pack expansion parameter with its ith element. Such an element, in the context of the instantiation, is interpreted as follows:

  • if the pack is a template parameter pack, the element is a template parameter of the corresponding kind (type or non-type) designating the type or value from the template argument;

So, for this example S<bool(*)(), bool>::N<Z> z; , instantiate Ts... would give a list bool(*)(), bool. Hence, the question can be simplified to:

template<template<bool(*)(), bool> class C>
struct Nested{};
template<int> struct Z {};
int main(){
  Nested<Z> z; // is well-formed?
}

Per temp.arg.template#3

A template-argument matches a template template-parameter P when P is at least as specialized as the template-argument A.

Does the argument Z match the parameter C? According to this rule temp.arg.template#4

A template template-parameter P is at least as specialized as a template template-argument A if, given the following rewrite to two function templates, the function template corresponding to P is at least as specialized as the function template corresponding to A according to the partial ordering rules for function templates. Given an invented class template X with the template parameter list of A (including default arguments):

  • Each of the two function templates has the same template parameters, respectively, as P or A.
  • Each function template has a single function parameter whose type is a specialization of X with template arguments corresponding to the template parameters from the respective function template where, for each template parameter PP in the template parameter list of the function template, a corresponding template argument AA is formed. If PP declares a parameter pack, then AA is the pack expansion PP... ([temp.variadic]); otherwise, AA is the id-expression PP.

If the rewrite produces an invalid type, then P is not at least as specialized as A.

That means, we have an invented template class X has the form:

template<int>
struct InventedX{};

Then, the rewritten function template for A has the form:

template<int N>
auto iventend_function(X<N>);

while the rewritten function template for P has the form:

template<bool(*A)(), bool B>
auto invented_function(X<A,B>) // invalid type for X<A,B>

So, P is not at least as specialized as A. Hence A cannot match with P. So, S<bool(*)(), bool>::N<Z> z; shall be ill-formed.

Mendiola answered 9/12, 2020 at 6:38 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.