Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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?

like image 899
Johannes Schaub - litb Avatar asked Nov 03 '16 17:11

Johannes Schaub - litb


2 Answers

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).

like image 117
Yakk - Adam Nevraumont Avatar answered Nov 19 '22 13:11

Yakk - Adam Nevraumont


Your second case is ill-formed; A<void(type<decltype(t)>...)>::f<0>(1) should be

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

For the first case, both compilers are behaving incorrectly; this was considered sufficiently confusing that CWG 1520 was raised to query the correct behavior; the conclusion was that pack expansion should be applied before alias substitution:

The latter interpretation (a list of specializations) is the correct interpretation; a parameter pack can't be substituted into anything, including an alias template specialization. CWG felt that this is clear enough in the current wording.

This is reminiscent of CWG 1558 (alias templates and SFINAE), which was fixed for C++14, but per the above even C++11 compilers are expected to get this correct, so it is disappointing that gcc and clang get it wrong (though in fairness they do behave correctly in simpler cases, including the motivating example in CWG 1520). Note that MSVC had a similar bug till recently; it is fixed in VS2015.

Your code (only in the first case) is correct; but as a workaround, you could alter your alias template to use and discard its template parameter, fixing your program for both compilers - of course that means that your CWG 1390 exploit will cease to be valid:

template<typename T> using type = decltype(((int(*)(T*))(0))(0)); // int

However, I don't think your CWG 1390 trick can work as presented, since even though the expansion-substitution of type<decltype(t)>... is not dependent on the types of t..., it is dependent on their number:

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

As Yakk points out, it can be made to work if you swap the member function template and data member, since a data member is OK in dependent context.

like image 3
ecatmur Avatar answered Nov 19 '22 14:11

ecatmur