P1061 introduced packs in structured binding declarations, an excellent improvement that allows tuple-like types to be converted into packs while bundling remaining elements.
auto [x, ...rest, z] = std::tuple<int,int,int>{0,1,2};
auto t = std::forward_as_tuple(rest...);
However, pack expansions cannot directly initialize structured bindings:
auto [x, ...rest, z] = args...; // currently unsupported and always ill-formed
That's correct. The idea of a structured binding is that you have one thing on the left, one thing on the right, and you're giving names (binding names) to the pieces of the left-hand thing.
So for example we would expect this to work—
template<class... Ts>
void f(Ts... ts) {
auto obj = ts...;
}
int main() { f(42); }
—because `ts...` expands to `42`, and `auto obj = 42;` is OK. The only reason it doesn't work today is a quirk of the grammar. (There's also a special rule that says a pack-expansion must be sufficiently general:
https://eel.is/c++draft/temp#res.general-6.5 forbids templates containing pack-expansions that
must contain zero elements. But this doesn't hit that rule because
this pack-expansion must contain
one element. So I believe the issue here is that the `
declaration` grammar doesn't permit an ellipsis in that position.)
Compare this, which does work fine:
template<class... Ts>
void f(Ts... ts) {
auto obj = auto(ts...);
}
int main() { f(42); }
Here, the grammar doesn't get in our way, and `auto(42)` gives us the initializer for `int obj`. But again `auto [...xs] = auto(ts...)` doesn't work, because `int obj` is not structured-bindable. (It doesn't have any "elements"; it's just an int.)
We can also write:
template<class... Ts>
void f(Ts... ts) {
auto obj = {ts...};
}
int main() { f(42); }
But this doesn't work with `auto [...xs]`, because here we deduce `std::initializer_list<int> obj`, and `initializer_list<int>` is not structured-bindable. (Semantically it doesn't know how many elements it has; and mechanically, the data members it does have — a pair of pointers — are private, not public.)
Now, I agree that in a vacuum it would be nice if we could have
auto [...xs] = {ts...};
For example,
auto [x, y] = {42, "hello"};
This doesn't work today because (1) {42, "hello"} has no type, and can't deduce a consistent `T` to put in `initializer_list<T>` either; and (2) `initializer_list` is not structured-bindable. But it does seem relatively obvious that the programmer wants to give the value 42 to `x` and "hello" to `y`, as if by
auto x = 42;
auto y = "hello";
The problem is, that's not what a structured binding declaration does. What it's supposed to do is: Create a single object on the left-hand side, and bind names to its pieces. What is the type of that single object here? It can't be std::tuple; that's a library type. It could be an ad-hoc unnamed standard-layout struct type — I think that could work. I wouldn't pursue that myself, but if you really really wanted this kind of thing, that's the direction I might consider exploring.
Oh, I forgot to add: Another avenue to explore would be the syntax
T [x,y] = rhs;
which today is disallowed — the type in front of a structured binding must involve a placeholder. But if an explicit type were permitted there, then you could write—
struct { int i; const char *s; } [x, y] = {42, "hello"};
Which is cumbersome; but from that vantage point you might start to wonder whether an appropriate shorthand syntax could be—
struct [x, y] = {42, "hello"};
struct [...xs] = {ts...};
This would mean "give me a struct with the appropriate (deduced) non-static data members, and
here (on the right-hand side) is a braced initializer to do its aggregate initialization." One downside of this fantasy syntax is that I don't see any way for it to take anything
but a braced initializer on the right-hand side. I'm not saying you'd ever
want anything but a braced initializer on the right (that is, after all, the point); but I do think that such inflexibility, such special-case-ness, feels like a red flag from the language designer's perspective.
None of these structured-binding syntaxes do — nor should — give you a way to simply rename a pack, e.g.
using ...xs = ts...;
// Now when I refer to xs...[0], I'm actually referring to the object ts...[0]
C++ doesn't let you "rename" variables today; the C++-ish way to do that is to bind a reference, as in `auto& x = t;`. So you could "rename" your pack of variables by binding them to a pack of references. The fantasy `struct [...xs]` syntax above does not provide any way to indicate that the data members of the struct should be references, so that's another strike against that particular fantasy syntax.
Today we can write:
void f(auto... ts) {
[](auto... xs) { (use(xs) , ...); }(ts); // make copies
[](auto&... xs) { (use(xs) , ...); }(ts); // take references
}
Perhaps you want tomorrow to be able to write:
void f(auto... ts) {
{ auto... xs = ts...; (use(xs) , ...); } // make copies
{ auto&... xs = ts...; (use(xs) , ...); } // take references
}
But that has nothing to do with structured bindings. It would require a proposal titled something like "A variable declaration can introduce a pack."
–Arthur