Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Definition of "pattern" for parameter pack expansion, especially within a function call

Tags:

c++

c++11

I understand that when an ellipsis (...) occurs to the right of a pattern containing a parameter pack, the pattern is expanded once for each parameter in the pack. However, though I have been able to find isolated examples of patterns with their expansion, I have been unable to find a definition of what constitutes a pattern. From what I can see, whitespace plays no role in the definition of the pattern, but parentheses do. For instance, in this example:

template<typename ... Ts>
void func(Ts)
{
    do_something(validate(Ts)...);
}

the do_something line would be expanded to:

    do_something(validate(var1), validate(var2), validate(var3))

if Ts happened to represent three variables. By contrast:

    do_something(validate(Ts...));

would be expanded to:

    do_something(validate(var1, var2, var3));

So clearly parentheses have something to do with determining where the pattern begins and ends. I can also see that whitespace does not. But that only gets me so far. I'd like to know exactly what constitutes a pattern, and how it will be expanded. I tried searching through the C++ Standard, but found too many instances of "parameter pack" to make that effective. Could someone please give me a definition of "pattern", or a link to a definition, or both?

UPDATE: To limit the scope of my question, I'd like to focus on the case where a pattern occurs within a function call. I've edited the title accordingly. Sorry I didn't make that clear from the beginning.

like image 629
Alan Avatar asked May 02 '16 13:05

Alan


2 Answers

A pattern is defined in the standard under [temp.variadic]/4: (via @t.c.)

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 ([dcl.fct]); the pattern is the parameter-declaration without the ellipsis.
  • In a template parameter pack that is a pack expansion ([temp.param]):
    • 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 ([dcl.init]); the pattern is an initializer-clause.
  • In a base-specifier-list (Clause [class.derived]; the pattern is a base-specifier.
  • In a mem-initializer-list ([class.base.init]) for a mem-initializer whose mem-initializer-id denotes a base class; the pattern is the mem-initializer.
  • In a template-argument-list ([temp.arg]); the pattern is a template-argument.
  • In a dynamic-exception-specification ([except.spec]); the pattern is a type-id.
  • In an attribute-list ([dcl.attr.grammar]); the pattern is an attribute.
  • In an alignment-specifier ([dcl.align]); the pattern is the alignment-specifier without the ellipsis.
  • In a capture-list ([expr.prim.lambda]); the pattern is a capture.
  • In a sizeof... expression ([expr.sizeof]); the pattern is an identifier.
  • In a fold-expression ([expr.prim.fold]); the pattern is the cast-expression that contains an unexpanded parameter pack.

The above quote from the standard draft talks about what part of the grammar described in the links is the "pattern" that is being expanded. To understand it, you need to know how the C++ grammar is described and the exceptions about how it is used in the standard text itself; however, if you have basic BNF knowledge and a bit of patience, you can work it out. The names are often useful as well.


However, you can use ... and mostly understand it without going nearly that deep.

The general rule is simple: you have some bit of C++ grammar (called the pattern) parsed in the usual way, where a pack of types is treated like a single type, and a pack of literals is treated like a single literal. Then, at the end of it, you have a ... expander. It then takes all of the unexpanded packs in the bit of C++ grammar immediately before (the pattern) and expands it.

How this works in each context is different; it isn't just a macro expansion. The contexts in which ... is valid are enumerated above; the effects of the expansion are listed in the standard at each point where it is valid.

In the most mundane use cases of ..., the pattern is an expression, and the expression gets expanded as-if it was each copy separated by , (not operator,, but the other "normal" one), usually in a context where a list of things is expected (function call, initializer list, etc).

There are function parameter declaration contexts (where ... both expands the types of the function parameters, and introduces a new pack of the paramter names), in template parameter lists (where it introduces a pack usually), etc.

sizeof... is a bit strange: for sizeof... it counts how many elements are in the pack passed in the (). This works differently, as the ... does not apply to the "structure on the left".

For alignas(X...), we end up with alignas(X@0), alignas(X@1), ... (where @0 is my pseudocode for the first element of the pack), as alignas(X@0, X@1, ...) is not valid C++ (again @T.C. in comments below).

For inheritance, it creates a set of base classes. For mem-initializer-lists, it lets you pass ctor arguments to said pack of base classes. For lambdas, it gives you limited capture of packs (not full on expression capture with expansion last I checked).

The pattern is the thing expanded. Importantly, an expanded pattern is not expanded by another pattern expander: so std::array< Ts, sizeof...(Ts) >... is a pack of arrays of various types, each one with a number of elements determined by how big the pack itself is. sizeof...(Ts) "counts as expanding" the Ts in its (), even though ... isn't to "its right", because the language defines the Ts in the () as the pattern that is expanded by the ....

The pattern in the general case cannot be called an expression, because types are not expressions, and some patterns are expressions (or at least expand into lists of expressions). And in some cases, ... expands a type-pattern into an expanded package of types (like in the throw expression).

The general rule, that you treat ... as expanding the thing on the left in an appropriate way in the local context, works for almost everything except sizeof... (which is a magical operator that tells you how many elements are in a parameter pack). It is only going to be in corner cases where this doesn't produce a decent model. And in my experience, at worse it will result in code that doesn't compile when you think it should; you can learn workarounds in that case. Like, remembering that a bare statement "has no local context", so you cannot do a = std::get<Is>(tup))...;, but rather the workaround (void)(int[]){0,(a = std::get<Is>(tup)),0)...}; (might have to typedef that int[]) where we provide a context (creating an array) for the pack expansion to work in.

C++1z's fold expressions are another quirky spot where ... does not apply on the left; here, they wanted to have an expansion of a binary operator, so "on the left" doesn't make as much sense as the usual "unary" expansion.

like image 70
Yakk - Adam Nevraumont Avatar answered Nov 14 '22 22:11

Yakk - Adam Nevraumont


The pattern is defined by its association with the ... expansion syntax. So validate(ts) alone is just a fragment of meaningless text. But validate(ts)... makes the validate(ts) part the pattern of the expansion. So it's a matter of how ... affects the associated code.

Usually, unpack syntax designates that the stuff to the left of the ... is the pattern. With C++17's fold expressions, this becomes a bit more complex. But generally speaking, if you want to know what the pattern is, find the ..., and look at the expression immediately to the left of it.

The specific limitations on what can be in the pattern depend entirely on where the expansion is taking place and what kind it of pack is involved (type vs. non-type, etc). The pattern needs to be whatever would be legal code for whatever context it is expanded within.

like image 25
Nicol Bolas Avatar answered Nov 14 '22 23:11

Nicol Bolas