Document #: | P1061R1 |
Date: | 2019-10-06 |
Project: | Programming Language C++ EWG |
Reply-to: |
Barry Revzin <barry.revzin@gmail.com> Jonathan Wakely <jonathan.wakely@gmail.com> |
R0 of this paper [P1061R0] was presented to EWGI in Kona 2019 [P1061R0.Minutes], who reviewed it favorably and thought this was a good investment of our time (4-3-4-1-0). The consensus in the room was that the restriction that the introduced pack need not be the trailing identifier.
Function parameter packs and tuples are conceptually very similar. Both are heterogeneous sequences of objects. Some problems are easier to solve with a parameter pack, some are easier to solve with a tuple
. Today, it’s trivial to convert a pack to a tuple
, but it’s somewhat more involved to convert a tuple
to a pack. You have to go through std::apply()
[N3915]:
This is great for cases where we just need to call a [non-overloaded] function or function object, but rapidly becomes much more awkward as we dial up the complexity. Not to mention if I want to return from the outer scope based on what these elements have to be.
How do we compute the dot product of two tuple
s? It’s a choose your own adventure of awkward choices:
Nested apply()
|
Using index_sequence
|
---|---|
Regardless of which option you dislike the least, both are limited to only std::tuple
s. We don’t have the ability to do this at all for any of the other kinds of types that can be used in a structured binding declaration [P0144R2] - because we need to explicit list the correct number of identifiers, and we might not know how many there are.
We propose to extend the structured bindings syntax to allow the user to introduce a pack as (at most) one of the identifiers:
std::tuple<X, Y, Z> f();
auto [x,y,z] = f(); // OK today
auto [...xs] = f(); // proposed: xs is a pack of length three containing an X, Y, and a Z
auto [x, ...rest] = f(); // proposed: x is an X, rest is a pack of length two (Y and Z)
auto [x,y,z, ...rest] = f(); // proposed: rest is an empty pack
auto [x, ...rest, z] = f(); // proposed: x is an X, rest is a pack of length one
// consisting of the Y, z is a Z
auto [...a, ...b] = f(); // ill-formed: multiple packs
If we additionally add the structured binding customization machinery to std::integer_sequence
, this could greatly simplify generic code:
Not only are these implementations more concise, but they are also more functional. I can just as easily use apply()
with user-defined types as I can with std::tuple
:
struct Point {
int x, y, z;
};
Point getPoint();
double calc(int, int, int);
double result = std::apply(calc, getPoint()); // ill-formed today, ok with proposed implementation
Python 2 had always allowed for a syntax similar to C++17 structured bindings, where you have to provide all the identifiers:
>>> a, b, c, d, e = range(5) # ok
>>> a, *b = range(3)
File "<stdin>", line 1
a, *b = range(3)
^
SyntaxError: invalid syntax
But you could not do any more than that. Python 3 went one step further by way of PEP-3132 [PEP.3132]. That proposal allowed for a single starred identifier to be used, which would bind to all the elements as necessary:
The Python 3 behavior is synonymous with what is being proposed here. Notably, from that PEP:
Possible changes discussed were:
- Only allow a starred expression as the last item in the exprlist. This would simplify the unpacking code a bit and allow for the starred expression to be assigned an iterator. This behavior was rejected because it would be too surprising.
R0 of this proposal only allowed a pack to be introduced as the last item, which was changed in R1.
Add a new grammar option for simple-declaration to 9 [dcl.dcl]:
sb-identifier:
...
identifiersb-identifier-list:
identifier
sb-identifier
sb-identifier-list,
identifier
sb-identifier-list,
sb-identifier
simple-declaration:
decl-specifier-seq init-declarator-listopt;
attribute-specifier-seq decl-specifier-seq init-declarator-list;
attribute-specifier-seqopt decl-specifier-seq ref-qualifieropt[
identifier-listsb-identifier-list]
initializer;
Change 9 [dcl.dcl] paragraph 8:
A simple-declaration with an
identifier-listsb-identifier-list is called a structured binding declaration ( [dcl.struct.bind]). The decl-specifier-seq shall contain only the type-specifierauto
and cv-qualifiers. The initializer shall be of the form “= assignment-expression”, of the form “{ assignment-expression }”, or of the form “( assignment-expression )”, where the assignment-expression is of array or non-union class type.
Change 9.5 [dcl.struct.bind] paragraph 1:
A structured binding declaration introduces the identifiers v0, v1, v2, … of theidentifier-listsb-identifier-list as names ([basic.scope.declarative]) of structured bindings. The declaration shall contain at most one sb-identifier. If the declaration contains an sb-identifier, the declaration introduces a structured binding pack ([temp.variadic]). Let cv denote the cv-qualifiers in the decl-specifier-seq.
Introduce a new paragraph after 9.5 [dcl.struct.bind] paragraph 1, introducing the term “structured binding size”:
The structured binding size of a type
E
is the required number of names that need to be introduced by the structured binding declaration, as defined below. If there is no structured binding pack, then the number of elements in the sb-identifier-list shall be equal to the structured binding size. Otherwise, the number of elements of the structured binding pack is the structured binding size less the number of elements in the sb-identifier-list.
Change 9.5 [dcl.struct.bind] paragraph 3 to define a structured binding size:
IfE
is an array type with element typeT
,the number of elements in the identifier-listthe structured binding size ofE
shall be equal to the number of elements ofE
.Each viThe ith identifier is the name of an lvalue that refers to the element i of the array and whose type isT
; the referenced type isT
.
Change 9.5 [dcl.struct.bind] paragraph 3 to define a structured binding size:
Otherwise, if the qualified-idstd::tuple_size<E>
names a complete type, the expressionstd::tuple_size<E>::value
shall be a well-formed integral constant expression and thenumber of elements in the identifier-liststructured binding size ofE
shall be equal to the value of that expression. […]Each viThe ith identifier is the name of an lvalue of typeTi
that refers to the object bound tori
; the referenced type isTi
.
Change 9.5 [dcl.struct.bind] paragraph 5 to define a structured binding size:
Otherwise, all ofE
’s non-static data members shall be direct members ofE
or of the same base class ofE
, well-formed when named ase.name
in the context of the structured binding,E
shall not have an anonymous union member, and thenumber of elements in the identifier-liststructured binding size ofE
shall be equal to the number of non-static data members ofE
. Designating the non-static data members ofE
asm0, m1, m2, . . .
(in declaration order),eachthe ith identifier is the name of an lvalue that refers to the membervi
mi
ofe
and whose type is cvTi
, whereTi
is the declared type of that member; the referenced type is cvTi
. The lvalue is a bit-field if that member is a bit-field.
Add a new clause to 13.6.3 [temp.variadic], after paragraph 3:
A structured binding pack is an identifier that introduces zero or more structured bindings ([dcl.struct.bind]). [ Example
auto foo() -> int(&)[2]; auto [...a] = foo(); // a is a structured binding pack containing 2 elements auto [b, c, ...d] = foo(); // d is a structured binding pack containing 0 elements auto [e, f, g, ...h] = foo(); // error: too many identifiers
- end example]
In 13.6.3 [temp.variadic], change paragraph 4:
A pack is a template parameter pack, a function parameter pack,
oran init-capture pack, or a structured binding pack. The number of elements of a template parameter pack or a function parameter pack is the number of arguments provided for the parameter pack. The number of elements of an init-capture pack is the number of elements in the pack expansion of its initializer.
In 13.6.3 [temp.variadic], paragraph 5 (describing pack expansions) remains unchanged.
In 13.6.3 [temp.variadic], add a bullet to paragraph 8:
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 ([temp.param]) of the corresponding kind (type or non-type) designating the ith corresponding type or value template argument;
- if the pack is a function parameter pack, the element is an id-expression designating the ith function parameter that resulted from instantiation of the function parameter pack declaration;
otherwise- if the pack is an init-capture pack, the element is an id-expression designating the variable introduced by the ithth init-capture that resulted from instantiation of the init-capture pack
.; otherwise- if the pack is a structured binding pack, the element is an id-expression designating the ith structured binding that resulted from the structured binding declaration.
Thanks to Michael Park and Tomasz Kamiński for their helpful feedback.
[N3915] Peter Sommerlad. 2014. apply() call a function with arguments from a tuple (V3).
https://wg21.link/n3915
[P0144R2] Herb Sutter. 2016. Structured Bindings.
https://wg21.link/p0144r2
[P1061R0] Barry Revzin, Jonathan Wakely. 2018. Structured Bindings can introduce a Pack.
https://wg21.link/p1061r0
[P1061R0.Minutes] EWGI. 2019. Kona 2019 EWGI: P1061R0.
http://wiki.edg.com/bin/view/Wg21kona2019/P1061
[PEP.3132] Georg Brandl. 2007. PEP 3132 – Extended Iterable Unpacking.
https://www.python.org/dev/peps/pep-3132/