How C++ expands multiple parameters packs simultaneously, - c++

Having following functions f0, f1, f2 in C++14 code, which accepts arbitrary number of fixed-length arrays:
#include <functional>
template<typename... TS, size_t N> void f0( TS(&& ... args)[N] ) {}
template<typename T, size_t... NS> void f1( T(&& ... args)[NS] ) {}
template<typename... TS, size_t... NS> void f2( TS(&& ... args)[NS] ) {}
int main(){
f0({1,2}, {3.0,4.0}, {true, false});
f1({1,2,3}, {4,5}, {6});
f2({1,2,3}, {4.0,5.0}, {true});
return 0;
}
Function f0 accepts arrays with different types and fixed array length. Function f1 accepts arrays with fixed type and different array lengths. It's clear how this works: C++ compiler deduces variable-length parameter pack in immediate context of template function instantiation, which is expanded in (&& ... args) expression.
Function f2 accepts arrays with different types and different array lengths, which produces two variable-length parameter packs, however there is only one ellipsis operator in pack expansion (&& ... args), but code compiles and works well.
So question is: what is general rule for expanding multiple parameter packs within single ellipsis operator? Obviously, at a minimum, they must be the same length, but what are the other requirements? Is there a precise definition that the n-th element of the first parameter packing should expand along with the n-th element of the second parameter packing?
Also, following code with explicit template argument provision does not compile: f2<int,float,bool,3,2,1>({1,2,3},{4.0f,5.0f},{true});. It would be interesting to know the reasons for this behaviour.

This is specified in C++ Standard section [temp.variadic]. Basically, it's what you described: when a pack expansion expands more than one pack, all those packs must have the same number of elements. And the expansion in most cases forms a list where the nth element in the resulting list uses the nth element of each expanded pack.
More exactly, paragraph 5 defines
A pack expansion consists of a pattern and an ellipsis, the instantiation of which produces zero or more instantiations of the pattern in a list (described below). The form of the pattern depends on the context in which the expansion occurs. Pack expansions can occur in the following contexts:
In a function parameter pack; the pattern is the parameter-declaration without the ellipsis.
...
In your example, each function template declares a function parameter pack named args. The patterns are TS(&& args)[N], T(&& args)[NS], and TS(&& args)[NS].
Paragraph 7 (after clarifying which packs are expanded by which pack expansions that when one pack expansion appears inside another) has the requirement
All of the packs expanded by a pack expansion shall have the same number of arguments specified.
And paragraph 8:
The instantiation of a pack expansion that is neither a sizeof... expression nor a fold-expression produces a list of elements E1, E2, ..., EN, where N is the number of elements in the pack expansion parameters. Each Ei is generated by instantiating the pattern and replacing each pack expansion parameter with its ith element.
So yes, for the instantiation of f3 where TS is deduced as int, double, bool and NS is deduced as 3, 2, 1, the pack expansion becomes a function parameter list with types int(&&)[3], double(&&)[2], bool(&&)[1].

All packs appearing as part of one pack expansion (...) must have exactly the same length. Otherwise substitution fails (which depending on context is a hard error or SFINAE). (see [temp.variadic]/7)
All packs are expanded so that the i-th expanded element of the pack expansion uses the i-th element of each pack. For the detailed expansion rule see [temp.variadic]/8.
(Links are to the post-C++20 draft of the standard, but the same applies to C++14.)

A template parameter pack is a template parameter that accepts zero or more template arguments (non-types, types, or templates). A function parameter pack is a function parameter that accepts zero or more function arguments.
A template with at least one parameter pack is called a variadic template.
Visit[1]: https://en.cppreference.com/w/cpp/language/parameter_pack
Visit[2]: Multiple expansions of multiple parameter packs in the same expression
Visit[3]: https://www.ibm.com/docs/en/zos/2.1.0?topic=only-variadic-templates-c11
I hope this help you.

Related

Exact rules for matching variadic template template parameters in partial template specialization

While creating this answer for another question I came around the following issue. Consider this program (godbolt):
#include <variant>
#include <iostream>
template <typename T>
struct TypeChecker {
void operator()() {
std::cout << "I am other type\n";
}
};
template<typename... Ts, template<typename...> typename V>
requires std::same_as<V<Ts...>, std::variant<Ts...>>
struct TypeChecker<V<Ts...>>
{
void operator()()
{
std::cout << "I am std::variant\n";
}
};
int main()
{
TypeChecker<std::variant<int, float>>{}();
TypeChecker<int>{}();
}
The output (which is also expected) is the following (with clang 14.0.0 as well as with gcc 12.1):
I am std::variant
I am other type
If however the three dots in the parameter list of the template template are removed, like this (whole program live on godbolt):
template<typename... Ts, template<typename> typename V>
,then the output is different for clang and gcc. The clang 14.0.0 compiled program outputs
I am other type
I am other type
whereas the gcc 12.1 compiled program outputs
I am std::variant
I am other type
It seems that using the non-variadic template template exhibits different matching rules in clang and gcc. So my question is, which behavior is correct if it is even well defined, and why?
Since defect report resolution P0522R0 was adopted, exact matching of template parameter lists for template template parameter match is no longer needed, and correct output according to the standard is:
I am std::variant
I am other type
In the current draft (which also contains changes related to C++20 concepts) relevant standard excerpts are temp.arg.template#3-4 (bold emphasis mine):
A template-argument matches a template template-parameter P when P is at least as specialized as the template-argument A. In this comparison, if P is unconstrained, the constraints on A are not considered. If P contains a template parameter pack, then A also matches P if each of A's template parameters matches the corresponding template parameter in the template-head of P. Two template parameters match if they are of the same kind (type, non-type, template), for non-type template-parameters, their types are equivalent ([temp.over.link]), and for template template-parameters, each of their corresponding template-parameters matches, recursively. When P's template-head contains a template parameter pack ([temp.variadic]), the template parameter pack will match zero or more template parameters or template parameter packs in the template-head of A with the same type and form as the template parameter pack in P (ignoring whether those template parameters are template parameter packs).
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-head of A (including default arguments and requires-clause, if any):
(4.1) Each of the two function templates has the same template parameters and requires-clause (if any), respectively, as P or A.
(4.2) 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-head of the function template, a corresponding template argument AA is formed. If PP declares a template 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.
So, as we see, exact (up to special rules for parameter pack) parameter matching is now considered only in the case parameter list of template template parameter contains a pack (like your original example), otherwise only the new at least as specialized as relation is used to test matching, which defines for both template template parameter and argument respective function templates and tests whether parameter-induced function is at least as specialized as argument-induced function according to partial ordering rules for template functions.
In particular, matching A=std::variant to P=template<typename> typename V we get these corresponding function templates:
template<typename...> class X;
template<typename T> void f(X<T>); // for P
template<typename... Ts> void f(X<Ts...>); // for A
So, to prove A matches P, we need to prove f(X<T>) is at least as specialized as f(X<Ts...>). temp.func.order#2-4 says:
Partial ordering selects which of two function templates is more specialized than the other by transforming each template in turn (see next paragraph) and performing template argument deduction using the function type. The deduction process determines whether one of the templates is more specialized than the other. If so, the more specialized template is the one chosen by the partial ordering process. If both deductions succeed, the partial ordering selects the more constrained template (if one exists) as determined below.
To produce the transformed template, for each type, non-type, or template template parameter (including template parameter packs thereof) synthesize a unique type, value, or class template respectively and substitute it for each occurrence of that parameter in the function type of the template. ...
Using the transformed function template's function type, perform type deduction against the other template as described in [temp.deduct.partial].
Transformed template of f(X<T>) is f(X<U1>) and of f(X<Ts...>) is f(X<U2FromPack>), where U1 and U2FromPack are two synthesized unique types. Now, temp.deduct.partial#2-4,8,10 says:
Two sets of types are used to determine the partial ordering. For each of the templates involved there is the original function type and the transformed function type.
[Note 1: The creation of the transformed type is described in [temp.func.order]. — end note]
The deduction process uses the transformed type as the argument template and the original type of the other template as the parameter template. This process is done twice for each type involved in the partial ordering comparison: once using the transformed template-1 as the argument template and template-2 as the parameter template and again using the transformed template-2 as the argument template and template-1 as the parameter template.
The types used to determine the ordering depend on the context in which the partial ordering is done:
(3.1) In the context of a function call, the types used are those function parameter types for which the function call has arguments.130
(3.2) In the context of a call to a conversion function, the return types of the conversion function templates are used.
(3.3) In other contexts the function template's function type is used.
Each type nominated above from the parameter template and the corresponding type from the argument template are used as the types of P and A.
Using the resulting types P and A, the deduction is then done as described in [temp.deduct.type]. ... If deduction succeeds for a given type, the type from the argument template is considered to be at least as specialized as the type from the parameter template.
Function template F is at least as specialized as function template G if, for each pair of types used to determine the ordering, the type from F is at least as specialized as the type from G. F is more specialized than G if F is at least as specialized as G and G is not at least as specialized as F.
Now, in our case, per 3.3, the function type itself is the only one considered among types to determine ordering. So, per 2, 8 and 10, to know whether f(X<T>) is at least as specialized as f(X<Ts...>) we need to see whether void(X<T>) is at least as specialized as void(X<Ts...>), or, equivalently, whether deduction from type for P=void(X<Ts...>) from A=void(X<U1>) succeeds. It does according to temp.deduct.type#9-10:
If P has a form that contains <T> or <i>, then each argument Pi
of the respective template argument list of P is compared with the corresponding argument Ai of the corresponding template argument list of A. If the template argument list of P contains a pack expansion that is not the last template argument, the entire template argument list is a non-deduced context. If Pi is a pack expansion, then the pattern of Pi is compared with each remaining argument in the template argument list of A. Each comparison deduces template arguments for subsequent positions in the template parameter packs expanded by Pi. ...
Similarly, if P has a form that contains (T), then each parameter type Pi of the respective parameter-type-list ([dcl.fct]) of P is compared with the corresponding parameter type Ai of the corresponding parameter-type-list of A. ...
Here, per 10, comparison of functions types results in a single comparison of X<Ts...> and X<U1>, which, according to 9, succeeds.
Thus, deduction is succesful, so f(X<T>) is indeed at least as specialized as f(X<Ts...>), so std::variant matches template<typename> typename V. Intuitively, we gave 'more general' template template argument which should work nicely for intended usage of a more specific template template parameter.
In practice, different compilers enable P0522R0 changes under different circumstances, and cppreference template parameters page (section Template template arguments) contains links and information on GCC, Cland and MSVC. In particular, GCC in C++17+ mode enables it by default (and for previous standards with compiler flag fnew-ttp-matching), but Clang doesn't in any mode unless -frelaxed-template-template-args flag is provided, thus you got the difference in output for them. With the flag, Clang also produces correct behaviour (godbolt).

Misunderstanding about non-deducible function template arguments

From C++ Templates - The Complete Guide 2nd edition:
Moreover, such parameters can't usefully be placed after a template parameter pack or appear in a partial specialization, because there would be no way to explicitly specify or deduce them.
template<typename ...Ts, int N>
void f(double (&)[N+1], Ts ... ps); // useless declaration because N
// cannot be specified or deduced
where such parameters refers (I think) to the template parameters corresponding to those template arguments that can never be deduced. I.e. in the example above N is the parameter that cannot be deduced because N+1 is "too complicated to be deduced".
But why specifying it is not possible? I understand that it's not possible to specify N and let ...Ts be deduced, but why isn't it possible to specify them all? In other words, what is wrong in specifying Ts=[int] and N=2 via the following?
double x[3];
f<int,2>(x,1);
I.e. in the example above N is the parameter that cannot be deduced because N+1 is "too complicated to be deduced".
Formally, this is [temp.deduct.type]/5.3
The non-deduced contexts are:
[...]
/5.3 A non-type template argument or an array bound in which a subexpression references a template parameter.
As is already covered in the following Q&A:
Which part of the C++ standard prevents explicitly specifying this template's arguments?
particularly that in a template-head of a function template
template<typename ...Ts, int N>
// ... function template
as per [temp.param]/14
A template parameter pack of a function template shall not be followed by another template parameter unless that template parameter can be deduced from the parameter-type-list ([dcl.fct]) of the function template or has a default argument ([temp.deduct]).
specifically as per the special rule for function templates (due to function template argument deduction), the template argument N must be deducible from the function's parameter list. As per [temp.deduct.type]/5.3, it is not, and f in the following example can never invoked (overload resolution will never consider it a viable candidate):
template<typename ...Ts, int N>
void f(double (&)[N+1], Ts ... ps);
whereas the following functions can both be found by overload resolution:
template<typename ...Ts, int N>
void g(double (&)[N], Ts ... ps); // N deducible from function parameter
template<typename ...Ts, int N = 2> // N has a default-template-argument
void h(double (&)[N+1], Ts ... ps);
But why specifying it is not possible?
As discussed in the linked to Q&A, although it "would make sense for a compiler to support this", the standard does not, and a leading template parameter pack greedily includes all explicitly provided template arguments, even those that would not be valid as part of the expanded pack (e.g. non-type template arguments to a type template parameter pack).

How can a template parameter pack have both explicit and deduced arguments?

g++, clang++, and MSVC (pre 2018) all accept the following C++17 code, resulting in the output "unsigned int" then "int":
#include <iostream>
void print_type(int) { std::cout << "int\n"; }
void print_type(unsigned int) { std::cout << "unsigned int\n"; }
template <typename ...T>
void print_types(T ...args)
{
(print_type(args),...);
}
int main()
{
print_types<unsigned int>(1, 1);
}
I agree that this ought to work this way, but I'm having trouble finding a description of why and exactly how in the Standard.
First there's [temp.deduct]/2 describing the processing of explicit template arguments before doing the rest of template argument deduction:
[T]he following steps are performed when evaluating an explicitly specified template argument list with respect to a given function template:
... There must not be more arguments than there are parameters unless at least one parameter is a template parameter pack, and there shall be an argument for each non-pack parameter....
The specified template argument values are substituted for the corresponding template parameters as specified below.
In the example, unsigned int is certainly a "specified template argument value". But if its "corresponding template parameter" T gets substituted now, it's difficult to see how it could become a longer list of types later.
For the template argument deduction process, there's [temp.deduct.call]/1:
For a function parameter pack that occurs at the end of the parameter-declaration-list, deduction is performed for each remaining argument of the call, taking the type P of the declarator-id of the function parameter pack as the corresponding function template parameter type. Each deduction deduces template arguments for subsequent positions in the template parameter packs expanded by the function parameter pack.
I take "remaining argument of the call" here to mean arguments after the ones that correspond to function parameters that are not the final function parameter pack. But that would mean that in my example, the first function argument 1 is used to deduce T=int. Does this deduction actually happen, but then get discarded/overridden by the T=unsigned int from the explicit template argument?
Or maybe "remaining argument of the call" is supposed to mean function arguments after the ones that do not correspond to the final function parameter pack AND after any that correspond to parameter types generated from explicit template arguments; and "subsequent positions in the template parameter packs expanded by the function parameter pack" is supposed to mean sequential positions after any filled by explicit template arguments, but this would be far from clear. And if so, it's also confusing that there is a list of parameter types associated with the function parameter pack but it's still a function parameter pack.
[Another possible implementation giving the expected behavior would be: when one or more explicit template arguments A_1, ..., A_k correspond to a template parameter pack P, invent another template parameter pack More_P of the same kind, and substitute each expansion of P with the template argument list {A_1, ..., A_k, More_P...}. Then More_P can be deduced like any other template parameter pack. If More_P is never deduced, substitute an empty list for all its expansions before evaluating semantics as for all other deduced substitutions. But there's even less justification for this interpretation in the Standard.]
Have I missed something in the Standard that better describes how explicit template arguments and deduced template arguments can work together to form a single list for one template parameter pack?
It's [temp.arg.explicit]/8:
Template argument deduction can extend the sequence of template arguments corresponding to a template parameter pack, even when the sequence contains explicitly specified template arguments. [ Example:
template<class ... Types> void f(Types ... values);
void g() {
f<int*, float*>(0, 0, 0); // Types is deduced to the sequence int*, float*, int
}
— end example ]

Is a heterogeneous variadic non-type template argument count flexible?

The following defines a variadic non-type nested class template, DEF. The non-type template parameters may be heterogeneous according to the type arguments provided for Ts.
template <typename ...Ts>
struct ABC {
template <Ts ...Xs>
struct DEF {};
};
A DEF object can be declared as follows:
ABC<int,bool>::DEF<17,true> x;
My question is, can the number of non-type template arguments provided to DEF be less than the number of type template arguments provided to ABC? For example, are either of these declarations valid:
ABC<int,bool>::DEF<17> y;
ABC<int,bool>::DEF< > z;
No, because Ts... is a pack-expansion.
§14.5.3 [temp.variadic]
p4 A pack expansion consists of a pattern and an ellipsis, the instantiation of which produces zero or more instantiations of the pattern in a list (described below). The form of the pattern depends on the context in which the expansion occurs. Pack expansions can occur in the following contexts:
[...]
In a template parameter pack that is a pack expansion (14.1):
if the template parameter pack is a parameter-declaration; the pattern is the parameter-declaration without the ellipsis;
[...]
p6 The instantiation of a pack expansion [...] produces a list E1, E2, ..., EN, where N is the number of elements in the pack expansion parameters. [...]
So both your examples would be ill-formed, since DEF will take exactly <int, bool>.

Expression contains unexpanded parameter packs

Somehow I don't get how variadic template parameter packs are expanded. What's wrong with thie following code?
#include <iostream>
template <typename T>
struct print_one
{
static void run(const T& t)
{
std::cout << t << ' ';
}
};
template<typename... Args>
void print_all(Args&&... args)
{
// the next line doesn't compile:
print_one<Args>::run(std::forward<Args>(args))...;
}
int main()
{
print_all(1.23, "foo");
}
Clang says, Expression contains unexpanded parameter packs 'Args' and 'args'. Why?
The ... has to go inside the function call parentheses:
print_one<Args>::run(std::forward<Args>(args)...);
Obviously, that won't work for your function that takes only a single argument, so you need to find a way to expand the calls into a function call or other allowed construct:
// constructing a dummy array via uniform initialization
// the extra 0 at the start is to make it work when the pack is empty
int dummy[]{0, (print_one<Args>::run(std::forward<Args>(args)), 0)...};
// or, if your compiler doesn't support uniform initialization
int dummy[] = {0, (print_one<Args>::run(std::forward<Args>(args)), 0)...};
// or, calling a dummy function
template<typename... Args> void dummy(Args...) {}
dummy((print_one<Args>::run(std::forward<Args>(args)), 0)...);
// or, constructing a temporary dummy object
struct dummy { dummy(std::initializer_list<int>) {} };
dummy{(print_one<Args>::run(std::forward<Args>(args)), 0)...};
// or, constructing a temporary initializer list
std::initializer_list<int>{(print_one<Args>::run(std::forward<Args>(args)), 0)...};
Note the use of the comma operator to turn the void return of print_one into a value suitable to place in an argument list or initializer expression.
The initializer-list forms are preferred to the function call forms, as they are (supposed to be) ordered LTR which function call arguments are not.
The forms where a parameter pack expansion can occur are covered by 14.5.3 [temp.variadic]:
4 - [...] Pack expansions can occur in the following contexts:
[...]
Your original code is illegal because although textually it might appear that it should produce a statement consisting of a number of comma-operator expressions, that is not a context allowed by 14.5.3:4.
The standard dictates where pack expansion is allowed:
§14.5.3 [temp.variadic] p4
[...] Pack expansions can occur in the following contexts:
In a function parameter pack (8.3.5); the pattern is the parameter-declaration without the ellipsis.
In a template parameter pack that is a pack expansion (14.1):
if the template parameter pack is a parameter-declaration; the pattern is the parameter-declaration without the ellipsis;
if the template parameter pack is a type-parameter with a template-parameter-list; the pattern is the corresponding type-parameter without the ellipsis.
In an initializer-list (8.5); the pattern is an initializer-clause.
In a base-specifier-list (Clause 10); the pattern is a base-specifier.
In a mem-initializer-list (12.6.2); the pattern is a mem-initializer.
In a template-argument-list (14.3); the pattern is a template-argument.
In a dynamic-exception-specification (15.4); the pattern is a type-id.
In an attribute-list (7.6.1); the pattern is an attribute.
In an alignment-specifier (7.6.2); the pattern is the alignment-specifier without the ellipsis.
In a capture-list (5.1.2); the pattern is a capture.
In a sizeof... expression (5.3.3); the pattern is an identifier.
So basically, as a top-level statement, expansion is not allowed. The rationale behind this? No idea. Most likely they only picked contexts where a seperating comma (,) is part of the grammar; anywhere else you might pick overloaded operator, if user-defined types are involved and get in trouble.