Johannes Schaub - litb Johannes Schaub - litb - 1 month ago 5
C++ Question

Is this "if e is a pack, then get a template name, otherwise get a variable name" valid or not?

I have tried to construct a case that requires no typename or template, but still yield a variable or template depending on whether a given name

t
is a function parameter pack or not

template<typename T> struct A { template<int> static void f(int) { } };
template<typename...T> struct A<void(T...,...)> { static const int f = 0; };
template<typename> using type = int;

template<typename T> void f(T t) { A<void(type<decltype(t)>...)>::f<0>(1); }

int main() {
f(1);
}


The above will refer to the
static const int
, and do a comparison. The following just has
T t
changed to be a pack and make
f
refer to a template, but GCC does not like either

template<typename ...T> void f(T ...t) { A<void(type<decltype(t)>...)>::f<0>(1); }

int main() {
f(1, 2, 3);
}


GCC complains for the first

main.cpp:5:68: error: incomplete type 'A<void(type<decltype (t)>, ...)>' used in nested name specifier
template<typename T> void f(T t) { A<void(type<decltype(t)>...)>::f<0>(1); }


And for the second

main.cpp:5:74: error: invalid operands of types '<unresolved overloaded function type>' and 'int' to binary 'operator<'
template<typename ...T> void f(T ...t) { A<void(type<decltype(t)>...)>::f<0>(1); }


I have multiple questions


  • Does the above code work according to the language, or is there an error?

  • Since Clang accepts both variants but GCC rejects, I wanted to ask what compiler is correct?

  • If I remove the body of the primary template, then for the
    f(1, 2, 3)
    case, Clang complains

    main.cpp:5:42: error: implicit instantiation of undefined template 'A<void (int)>'


    Please note that it says
    A<void (int) >
    , while I would expected
    A<void (int, int, int)>
    . How does this behavior occur? Is this a bug in my code - i.e is it illformed, or is it a bug in Clang? I seem to remember a defect report about the order of expansion vs the substitution of alias template, is that relevant and does it render my code ill-formed?


Answer

Expanding a parameter pack either should, or does, make an expression type dependent. Regardless of whether the things expanded are type dependent.

If it did not, there would be a gaping hole in the type dependency rules of C++ and it would be a defect in the standard.

So A<void(type<decltype(t)>...)>::f when t is a pack, no matter what tricks you pull in the void( here ) parts to unpack the t, should be a dependent type, and template is required before the f if it is a template.

In the case where t is not a pack, it is intended that type<decltype(t)> not be dependent (See http://www.open-std.org/jtc1/sc22/wg21/docs/cwg_active.html#1390), but the standard may or may not agree at this point (I think not?)

If compilers did "what the committee intended", then when t is not a pack:

A<void(type<decltype(t)>...)>::f<0>(1)

could mean

A<void(int...)>::f<0>(1)

which is

A<void(int, ...)>::f<0>(1)

and if f is a template (your code makes it an int, but I think swapping the two should work) this would be fine. But the standard apparently currently disagrees?

So if http://www.open-std.org/jtc1/sc22/wg21/docs/cwg_active.html#1390 was implemented, then you could swap your two A specializations. The void(T...,...) specialization should have a template<int> void f(int), and the T specialization should have a static const int.

Now in the case where A<> is dependent (on the size of a pack), ::f is an int and does not need template. In the case where A<> is not dependent, ::f is a template but does not need disambiguation.

We can replace the type<decltype(t)>... with:

decltype(sizeof(decltype(t)*))...

and sizeof(decltype(t)*) is of non-dependent type (it is std::size_t), decltype gives us a std::size_t, and the ... is treated as a old-school ... arg. This means void(std::size_t...) becomes a non-dependent type, so A<void(std::size_t...)> is not dependent, so ::f being a template is not a template in a dependent context.

In the case where t is a parameter pack with one element

decltype(sizeof(decltype(t)*))...

becomes

std::size_t

but in a dependent context (one copy per element in t pack). So we get

A<void(std::size_t)>::f

which is presumed to be a scalar value, so

A<void(std::size_t)>::f<0>(1)

becomes an expression evaluating to false.

(Chain of logic generated in a discussion with Johannes in comments in original question).