Discussion:
[std-proposals] Spread operator (...) for aggregate initialization
Jonas Lund
2018-11-11 21:06:43 UTC
Permalink
Rationale:
C++20 designated initialization for objects is a great quality of life
improvement but right now only goes half the way and with small changes
could promote constant usage and improve code quality.

In scenarios where an object copy is made but only a small part of the
members needs updating this could lead to far better code with relatively
small changes.

Example:

Given:
struct vec3 { float x,y,z; };
vec3 oldvec{1,2,3};



You can write (old):
vec3 newvec(oldvec);
newvec.y=20;



C++20 now lets you do:
vec3 newvec{ .x=oldvec.x , .y=20 , .z=oldvec.z };



This proposal would enable:
vec3 newvec{ ...oldvec , .y=20 };



Effort needed to implement:
Should be relatively low effort, no new keywords and C++20 already has
added designated initializers and this should be able to build on that.

Syntax rules:
Within an aggregate initialization while the rest operator (...) is
encountered at the beginning of a list (in the same place a designatede
initializer would be) the ... is expected to be followed by an value that
is the source value to pick members from, the compiler should allow for
multiple source values.
AFTER the last rest operator has been found there can only be additional
designated initializers.
(Putting spreads after designated initializers makes little sense if
following the semantics below)

IE:
T object * {* *.*designator *=* arg1 *,* *.*designator *{* arg2 *} *... *};*

would become
T object * {* ...sarg1, ...sarg2 , <SNIP> *.*designator *=* darg1 *,* *.*
designator *{* darg2 *} *<SNIP> *};*

(<SNIP> is used above since ... would be ambigious in this case)
sarg1, sarg2 are source objects whilst darg1 and darg2 are direct values.

Semantics:
The compiler should expand all members from each source arg(sarg) in
left-to-right order, the expanded members if overridden by other spreads or
explicitly given designated initializers should be ignored.
The spreading should occur on a visible OR public per-name basis, thus a
source-arg need not be of the same type as the object being initialized and
multiple objects of different types can be used to initialize one
destination object.
Spreading a member that does not exist in the destination must not be
allowed.
--
You received this message because you are subscribed to the Google Groups "ISO C++ Standard - Future Proposals" group.
To unsubscribe from this group and stop receiving emails from it, send an email to std-proposals+***@isocpp.org.
To post to this group, send email to std-***@isocpp.org.
To view this discussion on the web visit https://groups.google.com/a/isocpp.org/d/msgid/std-proposals/618d7960-6f9e-447d-9020-adc06a67652a%40isocpp.org.
Zhihao Yuan
2018-11-11 21:31:00 UTC
Permalink
Interesting, but I don't think cross-type, name-based
designated initialization makes sense, thus only one
spreader of the same type should be allowed.

There is another way to look at this. Let's consider
how do we designate initialize an aggregate with
base class:

struct Base { int a, b; };
struct Derived : Base { double c; };

Currently we can only do

Derived{ .c = 3.0 }

and Base is initialized with {}. We could treat
Base as a designater instead:

Derived{ .Base {1, 2}, .c = 3.0 }

Now if we treat Derived is its own base, we get

Derived{ .Derived{ 1, 2, 2.0 }, .c = 3.0 }

and .c = 3.0 should override 2.0, which is the
semantics you wanted.

--
Zhihao Yuan, ID lichray
The best way to predict the future is to invent it.
_______________________________________________

‐‐‐‐‐‐‐ Original Message ‐‐‐‐‐‐‐
The compiler should expand all members from each source arg(sarg) in left-to-right order, the expanded members if overridden by other spreads or explicitly given designated initializers should be ignored.
The spreading should occur on a visible OR public per-name basis, thus a source-arg need not be of the same type as the object being initialized and multiple objects of different types can be used to initialize one destination object.
Spreading a member that does not exist in the destination must not be allowed.
--
You received this message because you are subscribed to the Google Groups "ISO C++ Standard - Future Proposals" group.
To unsubscribe from this group and stop receiving emails from it, send an email to std-proposals+***@isocpp.org.
To post to this group, send email to std-***@isocpp.org.
To view this discussion on the web visit https://groups.google.com/a/isocpp.org/d/msgid/std-proposals/G4G3bioHm7a5pEl_MYWMqCr0bRKiUvIhJI7MJ-BfXmt5Mc3qF1gvhGv-DQJ4UWdVhnmySQ6-9_EOtxuqWaaWzr-8CDX6anYL55X03BHTQsw%3D%40miator.net.
Jonas Lund
2018-11-11 22:29:47 UTC
Permalink
Post by Zhihao Yuan
Interesting, but I don't think cross-type, name-based
designated initialization makes sense, thus only one
spreader of the same type should be allowed.
Depends on how one views the feature, name based could be useful for Q&D
conversion between libraries with similar types but conversion operators
might be a better fit for that use-case anyhow.
But i agree the potential for errors could possibly another option where to
restrict to same or parent classes as sources for initialization material
(we could spread all parent types with different inputs) and partial
overrides could also be useful (ie spread from the same type first then a
parent type to replace parts of the object).

There is another way to look at this. Let's consider
Post by Zhihao Yuan
how do we designate initialize an aggregate with
struct Base { int a, b; };
struct Derived : Base { double c; };
Currently we can only do
Derived{ .c = 3.0 }
and Base is initialized with {}. We could treat
Derived{ .Base {1, 2}, .c = 3.0 }
Now if we treat Derived is its own base, we get
Derived{ .Derived{ 1, 2, 2.0 }, .c = 3.0 }
and .c = 3.0 should override 2.0, which is the
semantics you wanted.
I kinda see the logic in this syntax but i don't think it gives much
benefits.
One would usually provide source-args as values that the compiler can infer
the type from so specifying it again would serve little purpose and the
rest operator ... has a similar semantic meaning in other contexts like
template pack spreading (and other languages also since this syntax would
be very similar to what exists in ES6/TS where it is in widespread usage
already)
--
You received this message because you are subscribed to the Google Groups "ISO C++ Standard - Future Proposals" group.
To unsubscribe from this group and stop receiving emails from it, send an email to std-proposals+***@isocpp.org.
To post to this group, send email to std-***@isocpp.org.
To view this discussion on the web visit https://groups.google.com/a/isocpp.org/d/msgid/std-proposals/3f9af11d-9d4f-43ea-8683-6e5b18f267d9%40isocpp.org.
Arthur O'Dwyer
2018-11-12 02:36:57 UTC
Permalink
Post by Zhihao Yuan
Interesting, but I don't think cross-type, name-based
Post by Zhihao Yuan
designated initialization makes sense, thus only one
spreader of the same type should be allowed.
Depends on how one views the feature, name based could be useful for Q&D
conversion between libraries with similar types but conversion operators
might be a better fit for that use-case anyhow.
But i agree the potential for errors could possibly another option where
to restrict to same or parent classes as sources for initialization
material (we could spread all parent types with different inputs) and
partial overrides could also be useful (ie spread from the same type first
then a parent type to replace parts of the object).
There is another way to look at this. Let's consider
Post by Zhihao Yuan
how do we designate initialize an aggregate with
struct Base { int a, b; };
struct Derived : Base { double c; };
Currently we can only do
Derived{ .c = 3.0 }
and Base is initialized with {}. We could treat
Derived{ .Base {1, 2}, .c = 3.0 }
Now if we treat Derived is its own base, we get
Derived{ .Derived{ 1, 2, 2.0 }, .c = 3.0 }
and .c = 3.0 should override 2.0, which is the
semantics you wanted.
I kinda see the logic in this syntax but i don't think it gives much
benefits.
One would usually provide source-args as values that the compiler can
infer the type from so specifying it again would serve little purpose and
the rest operator ... has a similar semantic meaning in other contexts like
template pack spreading (and other languages also since this syntax would
be very similar to what exists in ES6/TS where it is in widespread usage
already)
The main reason we'd want to add designated-initializer syntax to C++ is
for C99 compatibility. (So the *current* design of
C++-designated-initializer syntax is a bit of a dumpster fire in that
respect: last I checked, it doesn't get us C compatibility at all.)

One of the flagship features of C99 designated-initializer syntax is that
you can designate fields in any order, and later designators will override
earlier ones. So you can say for example

struct Margins {
int left, right;
int top, bottom;
};
#define MARGINS(...) (struct Margins){ .left=1, .right=1, .top=1,
.bottom=1, ##__VA_ARGS__ }
void pageout(struct Margins args);

int main() {
pageout(MARGINS(.right=17, .top=8));
}

and `pageout` will get called with `Margins{.left=1, .right=17, .top=8,
.bottom=1}`.

It is generally true in C++ that a class is responsible for its own
initialization — a derived class cannot "reach into" its parent class and
directly initialize any of its parents' data members. Initialization of the
parent's data members must be done by the constructor of the parent class
itself.

When Zhihao said, "...Now if we treat Derived is its own base...", he's
talking about the C++11 feature called *delegating constructors*. This is
what lets us write
https://godbolt.org/z/INq03o

struct Base { int a, b; };
struct Derived : Base {
double c;

Derived(int a, int b, double c) : Base{a,b}, c{c} {}
Derived(double c) *: Derived{1, 2, 2.0}* { c = 3.0; }
};

The problem with this (for C++) is that once we've told the compiler to
construct Base::a, Base::b, and Derived::c "according to how Derived{1, 2,
2.0} would have done it"... well, those three members will have been
constructed, and so we cannot tell the compiler to go back and
"re-construct" Derived::c using some different value *instead*. We can't
mix delegating constructors with member-initializers.

Derived(double c) : Derived{1, 2, 2.0}, *c{c}* {} *// ERROR*

Now, for designated initializers, we could throw all this philosophical
correctness in the trash and just say that we can re-designate members as
many times as we like, and only the last one will stick. (We need that
feature for C99 compatibility anyway.) So we could say that the following
is magically well-formed after all:

Derived x = {.Derived{1, 2, 2.0}, .c{c}};
// or equivalently
Derived x = {.Derived(1, 2, 2.0), .c = c};

Then, to get the effect of Jonas's "spread operator," our user would simply
delegate to the copy constructor of `Derived`, like this:

Derived x = {.Derived(oldx), .c = c};

Or, in terms of Jonas's own example:

vec3 newvec{ .vec3{oldvec}, .y=20 };

(I must point out: engineering-wise, this is not a well-designed vec3
class. Aggregates are basically never what you want in real code. We
shouldn't encourage people to write aggregates. We'd rather write a proper
constructor, so that we could say e.g.
vec3 newvec = oldvec.with_y(20);
However, since the designated initializers proposal seems to have broken
the levees and is headed for town, I figure I should do my part to say how
designated initializers *should* be done, before it becomes another of
these features where six months past the ship date we discover that it's
been done wrong.)

–Arthur
--
You received this message because you are subscribed to the Google Groups "ISO C++ Standard - Future Proposals" group.
To unsubscribe from this group and stop receiving emails from it, send an email to std-proposals+***@isocpp.org.
To post to this group, send email to std-***@isocpp.org.
To view this discussion on the web visit https://groups.google.com/a/isocpp.org/d/msgid/std-proposals/591529e2-a9db-41a2-9534-1638bf7e6101%40isocpp.org.
Zhihao Yuan
2018-11-12 07:30:30 UTC
Permalink
We designed a limited form of compatibility to allow
a library author to ship a header with designated
initialization that can be included in both C and C++
code. Copying a piece of C code and making it
compile in C++ has never been our goal.

Aggregates have nothing wrong in type design.
In one project that I recently work on, all the
alternative types for signaling some states in
std::variant are aggregates, so the total number
of aggregate types in our code easily exceeded
the types with constructors :)

--
Zhihao Yuan, ID lichray
The best way to predict the future is to invent it.
_______________________________________________

‐‐‐‐‐‐‐ Original Message ‐‐‐‐‐‐‐
The main reason we'd want to add designated-initializer syntax to C++ is for C99 compatibility. (So the current design of C++-designated-initializer syntax is a bit of a dumpster fire in that respect: last I checked, it doesn't get us C compatibility at all.)
(I must point out: engineering-wise, this is not a well-designed vec3 class. Aggregates are basically never what you want in real code. We shouldn't encourage people to write aggregates. [...])
--
You received this message because you are subscribed to the Google Groups "ISO C++ Standard - Future Proposals" group.
To unsubscribe from this group and stop receiving emails from it, send an email to std-proposals+***@isocpp.org.
To post to this group, send email to std-***@isocpp.org.
To view this discussion on the web visit https://groups.google.com/a/isocpp.org/d/msgid/std-proposals/8hYde2kmISHNM0Cfs9ekN8L16VEa2BqdU0onfDnK4t2rXh_KefM15W77lcbElZAotgMGj7FxLm76pm8d3XWZePbB7wUVA78Ot6L4MRK5dIg%3D%40miator.net.
Jonas Lund
2018-11-13 09:35:00 UTC
Permalink
Post by Arthur O'Dwyer
The main reason we'd want to add designated-initializer syntax to C++ is
for C99 compatibility. (So the *current* design of
C++-designated-initializer syntax is a bit of a dumpster fire in that
respect: last I checked, it doesn't get us C compatibility at all.)
One of the flagship features of C99 designated-initializer syntax is that
you can designate fields in any order, and later designators will override
earlier ones. So you can say for example
struct Margins {
int left, right;
int top, bottom;
};
#define MARGINS(...) (struct Margins){ .left=1, .right=1, .top=1,
.bottom=1, ##__VA_ARGS__ }
void pageout(struct Margins args);
int main() {
pageout(MARGINS(.right=17, .top=8));
}
and `pageout` will get called with `Margins{.left=1, .right=17, .top=8,
.bottom=1}`.
C99 compat is one thing but i also think one should consider where we get
redundant code today without it. One typical scenario is specifying options
for creating a window object for example (like this margins example), often
this operation has quite a lot of options due to the nature of windows on
various platforms but this usually leads to one of 3 bad scenarios:
1: untyped passing of options
2: many constructors
3: an options object passed that is built with a builder pattern

The first option is usually the least code but has it's downsides whilst
the last 2 options leads to quite a bit of code doing more or less the same
things.

Passing an options object with designated initializers would allow for only
those option values that are actually used in each instance to be passed
with the rest being initialized by the member initializer specification by
the option type and in this sense C++ does it better already since we don't
need any extra MARGINS macro.
Post by Arthur O'Dwyer
It is generally true in C++ that a class is responsible for its own
initialization — a derived class cannot "reach into" its parent class and
directly initialize any of its parents' data members. Initialization of the
parent's data members must be done by the constructor of the parent class
itself.
When Zhihao said, "...Now if we treat Derived is its own base...", he's
talking about the C++11 feature called *delegating constructors*. This is
what lets us write
https://godbolt.org/z/INq03o
struct Base { int a, b; };
struct Derived : Base {
double c;
Derived(int a, int b, double c) : Base{a,b}, c{c} {}
Derived(double c) *: Derived{1, 2, 2.0}* { c = 3.0; }
};
The problem with this (for C++) is that once we've told the compiler to
construct Base::a, Base::b, and Derived::c "according to how Derived{1, 2,
2.0} would have done it"... well, those three members will have been
constructed, and so we cannot tell the compiler to go back and
"re-construct" Derived::c using some different value *instead*. We can't
mix delegating constructors with member-initializers.
Derived(double c) : Derived{1, 2, 2.0}, *c{c}* {} *// ERROR*
Now, for designated initializers, we could throw all this philosophical
correctness in the trash and just say that we can re-designate members as
many times as we like, and only the last one will stick. (We need that
feature for C99 compatibility anyway.) So we could say that the following
Derived x = {.Derived{1, 2, 2.0}, .c{c}};
// or equivalently
Derived x = {.Derived(1, 2, 2.0), .c = c};
Then, to get the effect of Jonas's "spread operator," our user would
Derived x = {.Derived(oldx), .c = c};
vec3 newvec{ .vec3{oldvec}, .y=20 };
(I must point out: engineering-wise, this is not a well-designed vec3
class. Aggregates are basically never what you want in real code. We
shouldn't encourage people to write aggregates. We'd rather write a proper
constructor, so that we could say e.g.
vec3 newvec = oldvec.with_y(20);
However, since the designated initializers proposal seems to have broken
the levees and is headed for town, I figure I should do my part to say how
designated initializers *should* be done, before it becomes another of
these features where six months past the ship date we discover that it's
been done wrong.)
I'll admit i skimped on providing member defaults on that example so the
struct should've looked like this to begin with:

struct vec3 { float x=0,y=0,z=0; };

But why exactly is aggregates bad? Before members could have initializers
there was a risk values weren't defined (that's fixed now) and another
argument is that an outside initialization of data would miss including
values when the class was redefined so it was better to have it all
included in the class itself (the with_y clone function to ensure getting a
sane copy) and this is what a spread operator would fix.

So this proposal is basically to make plain-old-data into not really
needing anything more than just an aggregate without the past weakness that
was associated with it.

The vec3 given a spread operator being present could even be all-const:
struct vec3 { const float x=0,y=0,z=0; };

Without even impacting usability and would certainly be very defined and
safe.
--
You received this message because you are subscribed to the Google Groups "ISO C++ Standard - Future Proposals" group.
To unsubscribe from this group and stop receiving emails from it, send an email to std-proposals+***@isocpp.org.
To post to this group, send email to std-***@isocpp.org.
To view this discussion on the web visit https://groups.google.com/a/isocpp.org/d/msgid/std-proposals/4071ce61-ef01-4b90-934d-463a693abf09%40isocpp.org.
Jonas Lund
2018-11-13 09:45:20 UTC
Permalink
Post by Arthur O'Dwyer
However, since the designated initializers proposal seems to have broken
the levees and is headed for town, I figure I should do my part to say how
designated initializers *should* be done, before it becomes another of
these features where six months past the ship date we discover that it's
been done wrong.)
–Arthur
I'd like to add, a sanely defined variant of this is to be preferred and
having a limited variant (since the main benefit is to make aggregates
useful/safe) is totally fine in my opinion.

Could even be that this was kind of initialization was forbidden for
anything that isn't default copy/move constructed (since that'd imply the
same kind of member-to-member copying)
--
You received this message because you are subscribed to the Google Groups "ISO C++ Standard - Future Proposals" group.
To unsubscribe from this group and stop receiving emails from it, send an email to std-proposals+***@isocpp.org.
To post to this group, send email to std-***@isocpp.org.
To view this discussion on the web visit https://groups.google.com/a/isocpp.org/d/msgid/std-proposals/6c27bd08-1806-4d76-bf7c-d5aea7e36728%40isocpp.org.
Loading...