Designated initializer list status?

340 views
Skip to first unread message

land...@gmail.com

unread,
Nov 21, 2015, 9:11:37 AM11/21/15
to ISO C++ Standard - Future Proposals
Coming from Golang and C I was saddened by the fact that standard C++ lacks (JSON-esque) designated initializer list support.

Some pragmatic reasons to support it:
1. It seem to be some interest in this feature, e.g. 33 votes: http://stackoverflow.com/questions/18731707
2. It's makes C++ more compatible with C99.
3. Many compilers already seem have support for it. I realized it wasn't in C++ when I turned on --pedantic.

A contrived example:

    // ...

    class Link {
    public:
        int bw;
        Coord wr_c;
        Coord bl_c;
        int src;
        int mtime;
        Computer* dst;
        int u;
    };

    // ...

    int main() {
        // ...
        std::vector<Link> links(m);
        for (int i = 0; i < m; i++) {
            int a, b, c;
            std::cin >> a >> b >> c;
            links[i] = {c, Coord(), Coord(), a, 0, &comps[b]}; // Non-designated
            links[i] = {.src = a, .dst = &comps[b], .bw = c};  // Designated
        }
        // ...
    }

Some (likely highly subjective) problems with the non-designated syntax:
1. Order dependent (harder to read).
2. Does not scale well, parameter ordering is increasingly difficult to track when more fields are added.
3. Does not allow explicit initialization of a partial set of fields. (harder to read, annoying to write)

1, 2: Understanding the designated initializer list assignment requires no declaration or parameter order knowledge so the grammar is more context free and therefore easier to read.

3: You are forced to write an explicit constructor or enumerate defaults for all fields. An explicit constructor would require extra boilerplate and introduce another independent order you have to track (more context). Writing a constructor for a specific way you want to initialize a class seems like code smell.

So I hope this feature would become standardized. Thank you for reading my humble opinion.

Andrew Tomazos

unread,
Nov 21, 2015, 10:22:37 AM11/21/15
to std-pr...@isocpp.org

--

---
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-proposal...@isocpp.org.
To post to this group, send email to std-pr...@isocpp.org.
Visit this group at http://groups.google.com/a/isocpp.org/group/std-proposals/.

Nicol Bolas

unread,
Nov 21, 2015, 10:27:50 AM11/21/15
to ISO C++ Standard - Future Proposals, land...@gmail.com
On Saturday, November 21, 2015 at 9:11:37 AM UTC-5, land...@gmail.com wrote:
Coming from Golang and C I was saddened by the fact that standard C++ lacks (JSON-esque) designated initializer list support.

Some pragmatic reasons to support it:
1. It seem to be some interest in this feature, e.g. 33 votes: http://stackoverflow.com/questions/18731707

... and?
 
2. It's makes C++ more compatible with C99.

That's not a good reason for anything. C and C++ are different languages, with divergent goals.
 
3. Many compilers already seem have support for it. I realized it wasn't in C++ when I turned on --pedantic.

That's the best reason you've given thus far, but it's still pretty weak.
 

Says who? C++11 has always allowed it (the remainder are either value or default initialized, I forget which). And C++14 made it so that if you use default member initializers, they will be used if you don't specialize an initializer with braced-init-syntax:

struct A
{
 
int foo;
 
int bar = 7;
 
float jay = 12.784f;
};

A a
{5, 11};
a
.jay == 12.784f;

1, 2: Understanding the designated initializer list assignment requires no declaration or parameter order knowledge so the grammar is more context free and therefore easier to read.

3: You are forced to write an explicit constructor or enumerate defaults for all fields. An explicit constructor would require extra boilerplate and introduce another independent order you have to track (more context). Writing a constructor for a specific way you want to initialize a class seems like code smell.

And what about the rest of the idea? braced-init-lists in C++ don't work like they do in C, after all. You can use them in contexts where you select between different function overloads, picking the overload that has a constructor that matches the provided elements. How does that work with your designated initializers?
 
So I hope this feature would become standardized. Thank you for reading my humble opinion.

I hope you don't think that merely making a post to a mailing list will get it standardized.

Andrew Tomazos

unread,
Nov 21, 2015, 10:44:03 AM11/21/15
to std-pr...@isocpp.org
On Sat, Nov 21, 2015 at 4:27 PM, Nicol Bolas <jmck...@gmail.com> wrote:
On Saturday, November 21, 2015 at 9:11:37 AM UTC-5, land...@gmail.com wrote:
Coming from Golang and C I was saddened by the fact that standard C++ lacks (JSON-esque) designated initializer list support.

Some pragmatic reasons to support it:
1. It seem to be some interest in this feature, e.g. 33 votes: http://stackoverflow.com/questions/18731707

... and?
 
2. It's makes C++ more compatible with C99.

That's not a good reason for anything. C and C++ are different languages, with divergent goals.

C compatibility is very important for C++.  The fact that designated initializer lists have existing practice, especially in C, is a strong point in favor of the feature as a C++ feature.  It's not the only point considered though.

So I hope this feature would become standardized. Thank you for reading my humble opinion.

I hope you don't think that merely making a post to a mailing list will get it standardized.

Right.  This is at the stage where someone needs to write a detailed proposal and have it presented and discussed at a meeting.  Daryl Walker drafted one in 2013, but I don't think it was submitted and/or presented.

One of the things I've been thinking about is that we could have a std::designated_initializer_list unit type (no member functions) - and for aggregate class types a constructor taking such a type is generated.  A constructor taking such a type could also be explicitly defaulted.  We could then have a syntactic construct designated-init-list.

For example:

   struct S {
     S(std::designated_initializer_list) = default; // implicit for aggregate class types

     int x, y, z;
   }

   S s = {.x = 1, .y = 2, .z = 3};

   void f(S s);

   int main() { f({.x = 1, .y = 2, .z = 3}); }

     

land...@gmail.com

unread,
Nov 21, 2015, 11:15:51 AM11/21/15
to ISO C++ Standard - Future Proposals

On Saturday, November 21, 2015 at 4:27:50 PM UTC+1, Nicol Bolas wrote:
On Saturday, November 21, 2015 at 9:11:37 AM UTC-5, land...@gmail.com wrote:
Some (likely highly subjective) problems with the non-designated syntax:
1. Order dependent (harder to read).
2. Does not scale well, parameter ordering is increasingly difficult to track when more fields are added.
3. Does not allow explicit initialization of a partial set of fields. (harder to read, annoying to write)

Says who? C++11 has always allowed it (the remainder are either value or default initialized, I forget which). And C++14 made it so that if you use default member initializers, they will be used if you don't specialize an initializer with braced-init-syntax:

Yes that's great, I actually do that in my example: "u" is not specified in the non-designated syntax. But the "partial set" I mention could be fields that are not a prefix of the fields declared in the class so there is no suffix "reminder". E.g. you declare fields {0, 1, 2 .. 23} and you want to explicitly initialize the fields {14, 17, 23}.

On Saturday, November 21, 2015 at 4:44:03 PM UTC+1, Andrew Tomazos wrote:
One of the things I've been thinking about is that we could have a std::designated_initializer_list unit type (no member functions) - and for aggregate class types a constructor taking such a type is generated.  A constructor taking such a type could also be explicitly defaulted.  We could then have a syntactic construct designated-init-list.

For example:

   struct S {
     S(std::designated_initializer_list) = default; // implicit for aggregate class types

     int x, y, z;
   }

   S s = {.x = 1, .y = 2, .z = 3};

   void f(S s);

   int main() { f({.x = 1, .y = 2, .z = 3}); }

I like this idea.

Nicol Bolas

unread,
Nov 21, 2015, 12:56:49 PM11/21/15
to ISO C++ Standard - Future Proposals
On Saturday, November 21, 2015 at 10:44:03 AM UTC-5, Andrew Tomazos wrote:
On Sat, Nov 21, 2015 at 4:27 PM, Nicol Bolas <jmck...@gmail.com> wrote:
On Saturday, November 21, 2015 at 9:11:37 AM UTC-5, land...@gmail.com wrote:
Coming from Golang and C I was saddened by the fact that standard C++ lacks (JSON-esque) designated initializer list support.

Some pragmatic reasons to support it:
1. It seem to be some interest in this feature, e.g. 33 votes: http://stackoverflow.com/questions/18731707

... and?
 
2. It's makes C++ more compatible with C99.

That's not a good reason for anything. C and C++ are different languages, with divergent goals.

C compatibility is very important for C++.

The committee doesn't seem to think so. Or at least, not compatibility with features that aren't in C89. They don't want to have syntax step on each other, but they don't want to adopt features from C99/11 directly into C++ either.

The fact that designated initializer lists have existing practice, especially in C, is a strong point in favor of the feature as a C++ feature.

Only to the extent that it proves that the feature is viable. Which, admittedly, is a very useful thing to have.
 
  It's not the only point considered though.

So I hope this feature would become standardized. Thank you for reading my humble opinion.

I hope you don't think that merely making a post to a mailing list will get it standardized.

Right.  This is at the stage where someone needs to write a detailed proposal and have it presented and discussed at a meeting.  Daryl Walker drafted one in 2013, but I don't think it was submitted and/or presented.

It also wasn't particularly detailed. Or at least, not very well written; the details were all in spec-language wording rather than a human-readable explanation.

I would also suggest that any such proposal not have "improve parity with C" as its primary motivation (or in the case of the above, only motivation). More convincing motivations should revolve around using it in C++ code. What can you do with it that you can't do without it. The fact that it's in C is a good way to prove that the idea works, but that alone is hardly sufficient motivation for a C++ feature.

For example, there was a discussion awhile back about named parameters. It lead to the revelation that, if you have designated initializers, you can more or less get that. The function would take a single struct type, and you would call it with `funcName({.param1 = X, .param2 = Y, etc})`. Naturally, they wanted to add language functionality to remove the parens, but that's unnecessary. The main point is to give people an easy way to pass named parameters to a function, and designated initializers would allow precisely that.

Oh, and for someone coming up with such a proposal, don't forget that in C++17, types with base classes that have variables will also be considered aggregates. So you'll need to come up with a way to use them with designated initializers too.

Good luck solving that one ;)
 
One of the things I've been thinking about is that we could have a std::designated_initializer_list unit type (no member functions) - and for aggregate class types a constructor taking such a type is generated.  A constructor taking such a type could also be explicitly defaulted.  We could then have a syntactic construct designated-init-list.

I don't see the purpose of this. Generally speaking, most types that have constructors have invariants, so their data members are private, so you can't name them.

So when would you need such a constructor? Just say that if the braced-init-list contains designated initializers, it uses aggregate initialization instead of calling a constructor. And if the type is one that cannot use aggregate initialization, then it's a compiler error.

The only place where I see this being useful is for a type that makes everything public, but also provides constructors (and therefore can't use aggregate initialization). Is that a sufficiently common usage scenario for these? The point of a constructor is to provide and protect some kind of invariant; we have default member initializers for simply setting default values. So what was the point of providing that type with a constructor?

And don't forget: the last time we made special rules for how braced-init-lists call constructors, we got screwed with the `vector<int>/initializer_list<int>` problem. We shouldn't be making more rules that break uniformity farther than it already has been.

Andrew Tomazos

unread,
Nov 21, 2015, 1:37:28 PM11/21/15
to std-pr...@isocpp.org
On Sat, Nov 21, 2015 at 6:56 PM, Nicol Bolas <jmck...@gmail.com> wrote:
Oh, and for someone coming up with such a proposal, don't forget that in C++17, types with base classes that have variables will also be considered aggregates. So you'll need to come up with a way to use them with designated initializers too.

Good luck solving that one ;)

The simplest way would be to say that the base classes are default constructed and the designated initializer list can only refer to the most derived members.

struct D : B { int x; }
D d = { .x = 42}; // ok, B default constructed

Even simpler would say that the designated initializer list constructor is only generated for classes without base classes.

struct D : B { int x; }
D d = { .x = 42}; // ill-formed

Another solution would be to allow .BaseType as a key in a designated initializer list.

struct B { int y; }
struct D : B { int x; }
D d = { .B = {.y = 43}, .x = 42}; // ok

Another solution would be to roll up the base object graph into a single list of members and the designated initializer list addresses only member subobjects (and not base subobjects).

struct B { int y; }
struct D : B { int x; }
D d = { .x = 42, .y = 43}; // ok
 
The third and forth solutions are forward-compatible with the first and second solutions.

 
One of the things I've been thinking about is that we could have a std::designated_initializer_list unit type (no member functions) - and for aggregate class types a constructor taking such a type is generated.  A constructor taking such a type could also be explicitly defaulted.  We could then have a syntactic construct designated-init-list.

I don't see the purpose of this. Generally speaking, most types that have constructors have invariants, so their data members are private, so you can't name them.

Noone is suggesting being able to address private members.  The reason I suggest using a type, is for forwarding of designated initializer lists.  To actually consume them (as an endpoint) you need a compiler-generated constructor - either implicitly or explicitly defaulted.

eg

struct S { int x,y; };
std::vector<S> v;
v.push_back({.x = 42, .y = 43});

So what was the point of providing that type with a constructor?

Apart from the above, its also so we can address the designated initializer list constructor, to explicit default or explicitlty delete it.
 
And don't forget: the last time we made special rules for how braced-init-lists call constructors, we got screwed with the `vector<int>/initializer_list<int>` problem. We shouldn't be making more rules that break uniformity farther than it already has been.

I'm not sure what you mean by the  "`vector<int>/initializer_list<int>` problem".

Andrew Tomazos

unread,
Nov 21, 2015, 1:39:45 PM11/21/15
to std-pr...@isocpp.org
Sorry this is a bogus example.  I meant something like:

struct S { int x,y; };
std::optional<S> s;
s.emplace({.x = 42, .y = 43});

Nicol Bolas

unread,
Nov 21, 2015, 3:03:37 PM11/21/15
to ISO C++ Standard - Future Proposals


On Saturday, November 21, 2015 at 1:37:28 PM UTC-5, Andrew Tomazos wrote:
On Sat, Nov 21, 2015 at 6:56 PM, Nicol Bolas <jmck...@gmail.com> wrote:
Oh, and for someone coming up with such a proposal, don't forget that in C++17, types with base classes that have variables will also be considered aggregates. So you'll need to come up with a way to use them with designated initializers too.

Good luck solving that one ;)

The simplest way would be to say that the base classes are default constructed and the designated initializer list can only refer to the most derived members.
...
Even simpler would say that the designated initializer list constructor is only generated for classes without base classes.

Those aren't solutions.The committee went through a bunch of effort to allow braced-init-lists to work for aggregates with base class members.

Designated initializers are nothing more than a special form of aggregate initialization. Why should they be any less functional than regular aggregate initialization? You should not consider this case as something you can choose not to cover.

Another solution would be to roll up the base object graph into a single list of members and the designated initializer list addresses only member subobjects (and not base subobjects).

And what if the base class has a member of the same name as a derived class member?
 

 
One of the things I've been thinking about is that we could have a std::designated_initializer_list unit type (no member functions) - and for aggregate class types a constructor taking such a type is generated.  A constructor taking such a type could also be explicitly defaulted.  We could then have a syntactic construct designated-init-list.

I don't see the purpose of this. Generally speaking, most types that have constructors have invariants, so their data members are private, so you can't name them.

Noone is suggesting being able to address private members.  The reason I suggest using a type, is for forwarding of designated initializer lists.  To actually consume them (as an endpoint) you need a compiler-generated constructor - either implicitly or explicitly defaulted.

OK, you're now talking about a whole new problem, one that is completely unrelated to this one. Namely, solving perfect forwarding for construction via uniform initialization syntax. Being able to pass a braced-init-list to a (template) function, which will pass that braced-init-list to initialize a type.

Designated initializers are not special in needing a solution here. We need this for uniform initialization, whether designated or not. And designated initializers themselves don't need a solution to this problem for them to still be effective.

Focus on one problem at a time. You can't use braced-init-lists through `emplace` currently for aggregate initialization, so there's no reason to expect designated initializers to be able to work there either.


So what was the point of providing that type with a constructor?

Apart from the above, its also so we can address the designated initializer list constructor, to explicit default or explicitlty delete it.

I think you have a different vision for this.

My vision for designated initializatizers is this: aggregate initialization, where you can name the variable that gets a particular value. You seem to want something esle.

You say that having a constructor allows you to declare it deleted, so as to prevent using designated initializers for that type. Well... why would you want to? What purpose does it serve, what problem does it solve? You made your class members public. All designated initializer syntax is meant to do is make it easy to fill those members in.

So why would you ever want to forbid users from using this syntax?

The only cases I can come up with where a "designated initialization constructor" would be important would be a type that has both public members and constructors. A type with constructors is by definition not an aggregate and therefore cannot participate in aggregate initialization. So if you want to use designated initializers with it, you need some other way.

But there's a reason why we decided that types with constructors aren't aggregates. Type constructors exist to ensure invariants, to ensure the sanity of a type's interface and data. If all your members are public, there are no invariants. Which means there is no need for constructors. Oh, you might have some factory functions to make it easy to have different sets of default data, or to specify some data while taking defaults from elsewhere. Or whatever.

But if you're able to just initialize an object with whatever values you like, if it's just a bundle of independent variables with no invariant, then there is no point in that type having a constructor at all. So why would it not be an aggregate?

Give me an example of a type who's members are public (and therefore you'd want to use designated initializers) which also needs constructors. Or of a type who's members are public and that there is a good reason to want to forbid the use of designated initializers.

And don't forget: the last time we made special rules for how braced-init-lists call constructors, we got screwed with the `vector<int>/initializer_list<int>` problem. We shouldn't be making more rules that break uniformity farther than it already has been.

I'm not sure what you mean by the  "`vector<int>/initializer_list<int>` problem".

I'm referring to the fact that `vector<std::string> s{5};` will construct a vector with 5 empty strings, but `vector<int> s{5}` will construct a vector with a single integer with the value 5. This is because braced-init-list syntax prefers a matching `initializer_list` constructor over regular constructors. Template code has problems effectively using braced-init-lists because it can't know if it might call an initializer_list constructor instead of a regular one.

Indeed, one of the explicit examples for constexpr_if was to have an easy way to work around such issues, using braces or parenthesis on a type's initialization depending on whether the type `is_constructible` with the arguments.

As I said, the last time we started adding special rules for braced-init-list with constructors, we made uniform initialization non-uniform. Let's not do that again.

Andrew Tomazos

unread,
Nov 22, 2015, 5:47:26 PM11/22/15
to std-pr...@isocpp.org
Designated initializers are nothing more than a special form of aggregate initialization.
 
That's a premature design decision.  I agree generally it is desirable to have consistency between { braced-init-list, std::initializer_list, list-initialization } and { designated-init-list, std::designated_initializer_list, designated-list-initialization }.  It's the proposal authors job to work out the details.

And what if the base class has a member of the same name as a derived class member?
 
One option would be ill-formed ambiguous.  Another would be to favor the derived class.  Another (compatible with both previous) would be to allow the name to be qualified by .base_type::member_name.  It's a corner case though.  You shouldn't really "override" data members by name to begin with.



One of the things I've been thinking about is that we could have a std::designated_initializer_list unit type (no member functions) - and for aggregate class types a constructor taking such a type is generated.  A constructor taking such a type could also be explicitly defaulted.  We could then have a syntactic construct designated-init-list.

I don't see the purpose of this. Generally speaking, most types that have constructors have invariants, so their data members are private, so you can't name them.

Noone is suggesting being able to address private members.  The reason I suggest using a type, is for forwarding of designated initializer lists.  To actually consume them (as an endpoint) you need a compiler-generated constructor - either implicitly or explicitly defaulted.

OK, you're now talking about a whole new problem, one that is completely unrelated to this one. Namely, solving perfect forwarding for construction via uniform initialization syntax. Being able to pass a braced-init-list to a (template) function, which will pass that braced-init-list to initialize a type.

That wasn't my intent.  The intent was that the relationship between braced-init-list and std::initializer_list be somehow similar to that between designated-init-list and std::designated_initializer_list.

I see a braced-init-list as a compile-time ordered sequence (either homogeneous or heterogeneous).  It is the position index within the list that is used as the key.

I see a designated-init-list as a compile-time associative array.  It is the explicit member name designated that is the key.

With that difference in mind, designated-init-list should generally be integrated into the language in a similar way to how braced-init-list is.

Nicol Bolas

unread,
Nov 22, 2015, 9:54:27 PM11/22/15
to ISO C++ Standard - Future Proposals
On Sunday, November 22, 2015 at 5:47:26 PM UTC-5, Andrew Tomazos wrote:

Designated initializers are nothing more than a special form of aggregate initialization.
 
That's a premature design decision.

No it isn't. It is an answer to the question, "What do you think the feature is?" That is the most important question that a proposal can answer.

Believing that this is a premature question is how you get scope creep. It's how the pre-C++11 concepts kept accruing cruft until it grew so large that it collapsed under its own weight. It's how P0057 transformed from being a simple continuation mechanism into handing generators and even things that have nothing at all to do with concurrency or scheduling.

That kind of thinking leads to unfocused features, things that you can do "because they're kinda related" rather than because they're actually part of the design. It's bad design.

That's not to say that you shouldn't entertain different ways of achieving something. But every solution must ultimately match up with the question of what the feature is meant to accomplish. Deciding what the feature is must always be step #1.
 
I agree generally it is desirable to have consistency between { braced-init-list, std::initializer_list, list-initialization } and { designated-init-list, std::designated_initializer_list, designated-list-initialization }.  It's the proposal authors job to work out the details.

Or you could just... not do it this way. The feature of designated initializers does not in any way require what you're talking about. If you take away this and just make it a form of aggregate initialization, you will have lost nothing except for the ability to forward this initialization. Which, as previously stated, is a general problem with braced-init-lists, which need not be solved here.

It's easy to say that a proposal should be much more complicated than it needs to be, then say that it is someone else's job to figure out the actual details to make it work.

And what if the base class has a member of the same name as a derived class member?
 
One option would be ill-formed ambiguous.  Another would be to favor the derived class.  Another (compatible with both previous) would be to allow the name to be qualified by .base_type::member_name.  It's a corner case though.  You shouldn't really "override" data members by name to begin with.

You don't have to be overriding anything to get a conflict. Maybe the base class just happened to use the same variable name you wanted to use in your derived class. Or maybe you have two base classes that again just happen to have variables with the same name.

It may be a corner case, but C++ is a language of corner cases. Just because you don't like this case doesn't mean you can choose not to account for it.
 
One of the things I've been thinking about is that we could have a std::designated_initializer_list unit type (no member functions) - and for aggregate class types a constructor taking such a type is generated.  A constructor taking such a type could also be explicitly defaulted.  We could then have a syntactic construct designated-init-list.

I don't see the purpose of this. Generally speaking, most types that have constructors have invariants, so their data members are private, so you can't name them.

Noone is suggesting being able to address private members.  The reason I suggest using a type, is for forwarding of designated initializer lists.  To actually consume them (as an endpoint) you need a compiler-generated constructor - either implicitly or explicitly defaulted.

OK, you're now talking about a whole new problem, one that is completely unrelated to this one. Namely, solving perfect forwarding for construction via uniform initialization syntax. Being able to pass a braced-init-list to a (template) function, which will pass that braced-init-list to initialize a type.

That wasn't my intent.  The intent was that the relationship between braced-init-list and std::initializer_list be somehow similar to that between designated-init-list and std::designated_initializer_list.

Then your design does not express your intent, since they are nothing at all alike. An `initializer_list` is merely a pair of pointers to a specific type, which is backed by a temporary array created by the compiler. A `designated_initializer_list` isn't even a list. As you suggest, it's more like a map. But as I'll explain, it isn't even that.

I see a braced-init-list as a compile-time ordered sequence (either homogeneous or heterogeneous).  It is the position index within the list that is used as the key.

You may see it however you wish, but the actual standard does not agree. A braced-init-list is not even an expression, let alone an ordered sequence of values. It is a compile-time construct used to initialize a type, and it does not exist any longer than that.
 
I see a designated-init-list as a compile-time associative array.  It is the explicit member name designated that is the key.

... OK, I'll play along.

If the member name is the key, what is the "value" associated with that key?

Here's what I mean. Let's say I have this type:

struct T
{
 
string str;
  vector
<float> vec;
};

Which means I can do this:

T{.str = {"foo"}, .vec = {5, 0.3f}};

OK, now let's say I have some intermediate function:

void func(std::designated_initiailzer_list foo)
{
  T t
(foo);
}

func
({.str = {"foo"}, .vec = {5, 0.3f}});

OK, so... what happens here? At the point of the call to `func`, the compiler has no idea what `.str` and `.vec` refer to. And {"foo"} and {5, 0.3f} are braced-init-lists, which are neither values nor expressions. They are compiler constructs used to initialize a type, but there is no type associated with them. So... what gets stored in the `designated_initializer_list`?

You can't have some API that allows you to access members of a `designated_initializer_list` by name. Because that would require that you could get a C++ value from them, and they don't necessarily contain values.

The only way for your `designated_initializer_list` type to work is by pure compiler magic. That is, it's a completely opaque type, which at runtime will be filled in by "some data". And therefore, the only way you could use it is to pass it to someone else or to construct a type, as `func` does above.

And that of course leaves open the question of what happens if the user does this:

func({.str = {"foo"}, .vect = {5, 0.3f}});

T does not contain a `vect`. So... now what? Since `func` could be passed a `designated_initializer_list` from anywhere, I guess this becomes some kind of runtime exception thrown in compiler-generated code. But... that means that the construction of `T` within `func` has to be a runtime construction. That the compiler does not and cannot know what values get filled in and which ones don't.

How would `func` go about verifying that it can actually construct a `T` from a given `designated_initializer_list`? Would it have some API for doing that, like `d_i_l.can_construct<T>();`? How expensive would that function be? Almost certainly more expensive than regular old `vector::emplace`.

Or... we could avoid solving any of those problems and just make designated initialization a form of aggregate initialization.

Hannes

unread,
Nov 23, 2015, 5:50:06 AM11/23/15
to ISO C++ Standard - Future Proposals
On Monday, November 23, 2015 at 3:54:27 AM UTC+1, Nicol Bolas wrote:

Or... we could avoid solving any of those problems and just make designated initialization a form of aggregate initialization.

Yeah, I feel like that would be the simplest way to go now. Essentially making designated initialization syntactically equivalent to a (non-array) aggregate initialization where the ordering of keys is flexible and maintain the same C++14 behaviour as aggregate initialization for non-specified fields (default initialization).

That would be useful enough IMO and very easy to explain to people. "Oh, it's just aggregate initialization with some extra syntactic sugar."

Nicol Bolas

unread,
Nov 23, 2015, 9:41:06 AM11/23/15
to ISO C++ Standard - Future Proposals

There are three areas of concern when it comes to designated initializers in C++, which have to do with C++-specific features.

The first is base class member initialization. I think the best way to handle it syntactically is like this:

{ .base_class = {.mem1 = 5, .mem2 = {init}}, .outer1 = {1, 2, 3} };

That is, each class level gets its own initializer list. I think this is cleaner than doing `.base_class.mem1`, and it avoids the issue of classes in the hierarchy with the same name. It also makes it clear that you're setting members in a subobject.

Oh, and it allows you to initialize subobjects of the main class as you see fit. We could have initialized `.base_class` without naming its members, for example.

There is a downside to this approach. That downside is due to the second issue: how designated initializers interact with non-designated initializers in the same braced-init-list.

If you have this:

{3, "string", .member = 12};

How does that work? Do we initialize all of the designated members first, then order the rest and initialize them if they were not initialized? How does that interact with base class members?

I don't know much about designated initializers in C, so how do they handle it?

The third issue is overloading. Consider the following code:

struct agg1
{
   
int i; float f;
};

struct agg2
{
    std
::string s; std::string g;
};

void func(agg1) {std::cout << "func1\n";}
void func(agg2) {std::cout << "func2\n";}

int main() {
    func
({5, 2.0f});
   
return 0;
}

Under C++14 rules, this will actually select the correct overload to call, based on whether the type can be constructed given the braced-init-list. Designated initializers should not change this. Which means they should allow differentiation on the basis of member names:

struct agg1
{
   
int i; float f;
};

struct agg2
{
   
int i; float j;
};

void func(agg1) {std::cout << "func1\n";}
void func(agg2) {std::cout << "func2\n";}

int main() {
    func
({.i = 5, .j = 2.0f}); //Calls second overload.
   
return 0;
}

That's an extra bit of complexity.


Sean Middleditch

unread,
Nov 23, 2015, 12:05:29 PM11/23/15
to ISO C++ Standard - Future Proposals
On Monday, November 23, 2015 at 6:41:06 AM UTC-8, Nicol Bolas wrote:
On Monday, November 23, 2015 at 5:50:06 AM UTC-5, Hannes wrote:
On Monday, November 23, 2015 at 3:54:27 AM UTC+1, Nicol Bolas wrote:

Or... we could avoid solving any of those problems and just make designated initialization a form of aggregate initialization.

Yeah, I feel like that would be the simplest way to go now. Essentially making designated initialization syntactically equivalent to a (non-array) aggregate initialization where the ordering of keys is flexible and maintain the same C++14 behaviour as aggregate initialization for non-specified fields (default initialization).

That would be useful enough IMO and very easy to explain to people. "Oh, it's just aggregate initialization with some extra syntactic sugar."

There are three areas of concern when it comes to designated initializers in C++, which have to do with C++-specific features.

The first is base class member initialization. I think the best way to handle it syntactically is like this:

{ .base_class = {.mem1 = 5, .mem2 = {init}}, .outer1 = {1, 2, 3} };

That is, each class level gets its own initializer list. I think this is cleaner than doing `.base_class.mem1`, and it avoids the issue of classes in the hierarchy with the same name. It also makes it clear that you're setting members in a subobject.

What about base initializes for:

  struct derived : base<int>, base<float> { ... };

You can't use the base type's name anymore as a disambiguator. Would the initializer specification have to allow specifying the template specialization?

Does the base initialization support aliased names?
 

Oh, and it allows you to initialize subobjects of the main class as you see fit. We could have initialized `.base_class` without naming its members, for example.

There is a downside to this approach. That downside is due to the second issue: how designated initializers interact with non-designated initializers in the same braced-init-list.

If you have this:

{3, "string", .member = 12};

How does that work? Do we initialize all of the designated members first, then order the rest and initialize them if they were not initialized? How does that interact with base class members?

I would imagine it would work identically to defaulted function parameters - they're just call-sight syntactical helpers and have no effect on the callee's code at all.

Relatedly, think of constructors. A syntax that only works for initializing a type with public members is not _that_ useful (though it's certainly not useless). I think that we need to solve named parameters first (preferably IMO using a similar syntax to C99's designated initializers) so that you can use the exact same syntax for initializing a simple type as you do for initializing a class with user-defined constructors.

I say that requires the default arguments solution because initializing a class with constructors requires letting a class designer mark a constructor's parameter names as being part of the API (because we certainly don't want to do that always!), which is 99.98% the remaining major design problem with named function parameters.

I'm currently a fan of just using the .name syntax in both the declaration and call-site, which keeps syntactical parity with C99 designated initializers, but there's still some hairier corners of C++ I haven't thought through fully yet, so I'm not sure if that approach works.

It'd be "nice" IMO though to get:

(a) function(.foo = 1, .bar = "gaz"); // invoke a function
(b) constructor{.foo = 1, .bar = "gaz"); // construct a temporary
(c) type name = {.foo = 1, .bar = "gaz"}; // initialize a local
(d) void declaration(int .foo, string .bar); // declare a function/constructor
 
I'm still rolling it around in my head, though.


I don't know much about designated initializers in C, so how do they handle it?

The third issue is overloading. Consider the following code:

Overloading is indeed the tricky bit that needs a lot of careful thought and study from anyone planning to actually propose any of this.

Nicol Bolas

unread,
Nov 23, 2015, 2:04:20 PM11/23/15
to ISO C++ Standard - Future Proposals
On Monday, November 23, 2015 at 12:05:29 PM UTC-5, Sean Middleditch wrote:
On Monday, November 23, 2015 at 6:41:06 AM UTC-8, Nicol Bolas wrote:
On Monday, November 23, 2015 at 5:50:06 AM UTC-5, Hannes wrote:
On Monday, November 23, 2015 at 3:54:27 AM UTC+1, Nicol Bolas wrote:

Or... we could avoid solving any of those problems and just make designated initialization a form of aggregate initialization.

Yeah, I feel like that would be the simplest way to go now. Essentially making designated initialization syntactically equivalent to a (non-array) aggregate initialization where the ordering of keys is flexible and maintain the same C++14 behaviour as aggregate initialization for non-specified fields (default initialization).

That would be useful enough IMO and very easy to explain to people. "Oh, it's just aggregate initialization with some extra syntactic sugar."

There are three areas of concern when it comes to designated initializers in C++, which have to do with C++-specific features.

The first is base class member initialization. I think the best way to handle it syntactically is like this:

{ .base_class = {.mem1 = 5, .mem2 = {init}}, .outer1 = {1, 2, 3} };

That is, each class level gets its own initializer list. I think this is cleaner than doing `.base_class.mem1`, and it avoids the issue of classes in the hierarchy with the same name. It also makes it clear that you're setting members in a subobject.

What about base initializes for:

  struct derived : base<int>, base<float> { ... };

You can't use the base type's name anymore as a disambiguator. Would the initializer specification have to allow specifying the template specialization?

The type names for the base classes are `base<int>` and `base<float>`. `base` itself is not a typename; it's the name of a template.
 
Relatedly, think of constructors. A syntax that only works for initializing a type with public members is not _that_ useful (though it's certainly not useless). I think that we need to solve named parameters first (preferably IMO using a similar syntax to C99's designated initializers) so that you can use the exact same syntax for initializing a simple type as you do for initializing a class with user-defined constructors.

Perfect is the enemy of good. I don't see a need to make designated initializers dependent on general named parameter solutions.

Besides, the committee seems to hate named parameters on general principle. And there will be general resistance to designated initializers out of fears that it will lead to de-facto named parameter usage by using structs and designated initializers.

Our goal should be to avoid giving them that impression.

Also, we could add wording to allow brace elision to save you:

class SomeType
{
public:
 
struct ConstructorFields{ int foo; vector<int> bar; float def = 42.0f}
 
SomeType(ConstructorFields cf);
 
SomeType(int i);

private:
};

SomeType t = {.foo = 4, .bar = {1, 2, 3}};

Adding another set of braces would have allowed this to work. We just need wording to make those braces unnecessary, if the braced-init-list doesn't match up with `SomeType`'s argument list, but it does match up with one of the single-argument constructors. And designated initializers by definition cannot call a constructor, so they never match.

Then again, we could just make people use double braces, relying on existing brace elision rules to get rid of them:

SomeType t1 = {{5}}; //Calls single integer constructor.
SomeType t2 = {{.foo = 4, .bar = {1, 2, 3}}}; //Calls named parameter constructor.


Hannes Landeholm

unread,
Nov 23, 2015, 2:14:21 PM11/23/15
to std-pr...@isocpp.org
On 23 November 2015 at 15:41, Nicol Bolas <jmck...@gmail.com> wrote:
There are three areas of concern when it comes to designated initializers in C++, which have to do with C++-specific features.

The first is base class member initialization. I think the best way to handle it syntactically is like this:

{ .base_class = {.mem1 = 5, .mem2 = {init}}, .outer1 = {1, 2, 3} };

That is, each class level gets its own initializer list. I think this is cleaner than doing `.base_class.mem1`, and it avoids the issue of classes in the hierarchy with the same name. It also makes it clear that you're setting members in a subobject.

Oh, and it allows you to initialize subobjects of the main class as you see fit. We could have initialized `.base_class` without naming its members, for example.

There is a downside to this approach. That downside is due to the second issue: how designated initializers interact with non-designated initializers in the same braced-init-list.

If you have this:

{3, "string", .member = 12};

How does that work? Do we initialize all of the designated members first, then order the rest and initialize them if they were not initialized? How does that interact with base class members?

I don't know much about designated initializers in C, so how do they handle it?



17 Each brace-enclosed initializer list has an associated current object. When no designations are present, subobjects of the current object are initialized in order according to the type of the current object: array elements in increasing subscript order, structure members in declaration order, and the first named member of a union.148) In contrast, a designation causes the following initializer to begin initialization of the subobject described by the designator. Initialization then continues forward in order, beginning with the next subobject after that described by the designator.149)

18 Each designator list begins its description with the current object associated with the closest surrounding brace pair. Each item in the designator list (in order) specifies a particular member of its current object and changes the current object for the next designator (if any) to be that member.150) The current object that results at the end of the designator list is the subobject to be initialized by the following initializer. 

19 The initialization shall occur in initializer list order, each initializer provided for a particular subobject overriding any previously listed initializer for the same subobject;151) all subobjects that are not initialized explicitly shall be initialized implicitly the same as objects that have static storage duration.

Essentially C has a "current object" that starts at the first declared member of a struct or the first named member of a union. The "current object" is then incremented forward for each non-designated item in the initialization list. In contrast, a designated item overrides that "current object". If this cases a member to be initialized twice it is well defined per (19) that the later initializer overrides the previous one even though a later rule states that the evaluation order of the statements are undefined (23).

However compilers prints a warning if you are overriding previously initialized fields (clang: -Winitializer-overrides) so this is likely frowned upon even if it's well defined behaviour. I don't see any benefit in having C++ support this.

Ville Voutilainen

unread,
Nov 23, 2015, 2:17:27 PM11/23/15
to ISO C++ Standard - Future Proposals
On 23 November 2015 at 21:04, Nicol Bolas <jmck...@gmail.com> wrote:
> Besides, the committee seems to hate named parameters on general principle.

Correction: the committee has multiple times rejected proposals that don't work.
The parameter names in the library are unspecified, which means that none
of the proposals seen thus far would work with the library. Various aspiring
proposal authors continue to cite e.g. Common Lisp and Python as examples
where named parameters "just work". The difference there is three-fold:
1) in those languages, the library parameter names are well-specified
2) those languages do not suffer from preprocessor macros leaking into
the library implementation, whether standard library or not
3) library authors, even authors of non-standard libraries, have reservations
of having to start supporting stable names once the names become part
of the interface.

All proposals for named parameters have thus far failed to reconcile
those concerns,
regardless of the actual syntax they have proposed.

Nicol Bolas

unread,
Nov 23, 2015, 3:33:58 PM11/23/15
to ISO C++ Standard - Future Proposals

C++ most assuredly should make multiple initializations to the same field be an error. That being said, I like the idea of the "current object" increment. It makes it clear how designated initializers work with normal aggregate initialization, as well as allowing default member initializers to still work if you jump around them.

Nicol Bolas

unread,
Nov 23, 2015, 3:34:28 PM11/23/15
to ISO C++ Standard - Future Proposals

Six on one hand, half-a-dozen on the other. Having named parameters means parameter names are part of a function's interface. So if people are against that, then they're against named parameters.

That being said, I cannot verify if any of the named parameter proposals were ever opt-in. That is, that a function declaration had to use specific syntax to allow itself to be used with named parameters. All of the ones I found seem to assume that any function that gives its parameters names must be callable with named parameters.

Requiring opt-in on a per-function basis would seem to satisfy #3: each library author decides if they want to allow the use of named parameters for each function. And if they do, then it is their responsibility to keep the names legitimate.

#2 is hardly a fair concern to castigate named parameters over. Macro leakage potentially hurts lots of things, from uses of user-defined literals to many other features. Modules will do what can be done to prevent this, but we shouldn't avoid features out of fear of what macro leakage will cause.

Thiago Macieira

unread,
Nov 23, 2015, 6:33:59 PM11/23/15
to std-pr...@isocpp.org
On Monday 23 November 2015 12:34:27 Nicol Bolas wrote:
> Six on one hand, half-a-dozen on the other. Having named parameters means
> parameter names are part of a function's interface. So if people are
> against that, then they're against named parameters.

They're against making the ugly parameters currently used for various reasons
become API.

Examples:
libstdc++:
basic_string(const basic_string& __str, size_type __pos,
size_type __n, const _Alloc& __a)

libc++:
basic_string(const basic_string& __str, size_type __pos, size_type __n =
npos,
const allocator_type& __a = allocator_type());

MSVC (Dinkumware):
basic_string(const _Myt& _Right, size_type _Roff, size_type _Count,
const _Alloc& _Al)

Note how all three implementations use names reserved to the compiler (double
underscore or underscore + capital) to avoid someone #define'ing them and
causing problems.

This code does not compile on Solaris:

int sun, mercury, venus, earth, mars, jupiter, saturn, uranus, neptune;

--
Thiago Macieira - thiago (AT) macieira.info - thiago (AT) kde.org
Software Architect - Intel Open Source Technology Center
PGP/GPG: 0x6EF45358; fingerprint:
E067 918B B660 DBD1 105C 966C 33F5 F005 6EF4 5358

Thiago Macieira

unread,
Nov 23, 2015, 6:48:10 PM11/23/15
to std-pr...@isocpp.org
On Monday 23 November 2015 20:14:20 Hannes Landeholm wrote:
> Essentially C has a "current object" that starts at the first declared
> member of a struct or the first named member of a union. The "current
> object" is then incremented forward for each non-designated item in the
> initialization list. In contrast, a designated item overrides that "current
> object". If this cases a member to be initialized twice it is well defined
> per (19) that the later initializer overrides the previous one even though
> a later rule states that the evaluation order of the statements are
> undefined (23).

That means in the following:

struct S { int i; };
struct S s = { .i = f(1), .i = f(2) };

It is undefined in which order the function f() will be called, but it will be
called twice and the value of s.i will be the result of f(2).

Since all types in C are POD, double assignment can be eliminated.

I believe it follows that using the object being initialised in the
initialiser list is undefined behaviour. That is:

struct T { int i, j; };
struct T s = { 1, f(s.i) };

Though I can't actually put my finger on what the cause of UB is. It could be
just reading from an uninitialised variable, which is just implementation-
defined behaviour up until C++14.

Nicol Bolas

unread,
Nov 23, 2015, 10:47:39 PM11/23/15
to ISO C++ Standard - Future Proposals
On Monday, November 23, 2015 at 6:33:59 PM UTC-5, Thiago Macieira wrote:
On Monday 23 November 2015 12:34:27 Nicol Bolas wrote:
> Six on one hand, half-a-dozen on the other. Having named parameters means
> parameter names are part of a function's interface. So if people are
> against that, then they're against named parameters.

They're against making the ugly parameters currently used for various reasons
become API.

But as has been previously stated, the standard doesn't define those parameter names. So they wouldn't be able to become part of the API. They would be implementation defined, rather than standard defined.

And I'd point out that that nobody seems to be willing to stop concepts from getting into C++17 just because it hasn't been applied to the standard library.

But then again, the whole named parameters discussion is off-topic: this is about designated initializers, which should not be conflated with named parameters. After all, structure member names are already part of a struct's API.

Sean Middleditch

unread,
Nov 23, 2015, 11:03:35 PM11/23/15
to std-pr...@isocpp.org

On Nov 23, 2015 7:47 PM, "Nicol Bolas" <jmck...@gmail.com> wrote:
>
> But then again, the whole named parameters discussion is off-topic: this is about designated initializers, which should not be conflated with named parameters. After all, structure member names are already part of a struct's API.

They are intimately related because of constructors.

You can't have good C++-y designated initializers supporting user-designed classes without dealing with constructor parameters and their names at some level.

>
> --
>
> ---
> You received this message because you are subscribed to a topic in the Google Groups "ISO C++ Standard - Future Proposals" group.
> To unsubscribe from this topic, visit https://groups.google.com/a/isocpp.org/d/topic/std-proposals/875H9Elkhdw/unsubscribe.
> To unsubscribe from this group and all its topics, send an email to std-proposal...@isocpp.org.

Nicol Bolas

unread,
Nov 23, 2015, 11:47:47 PM11/23/15
to ISO C++ Standard - Future Proposals


On Monday, November 23, 2015 at 11:03:35 PM UTC-5, Sean Middleditch wrote:

On Nov 23, 2015 7:47 PM, "Nicol Bolas" <jmck...@gmail.com> wrote:
>
> But then again, the whole named parameters discussion is off-topic: this is about designated initializers, which should not be conflated with named parameters. After all, structure member names are already part of a struct's API.

They are intimately related because of constructors.

You can't have good C++-y designated initializers supporting user-designed classes without dealing with constructor parameters and their names at some level.


Why not? I already explained how to get the equivalent behavior without having actual named parameters (have the type take a struct and use an extra set of braces).

Designated initializers name fields in data structures, not parameters for constructors. They're not the same thing, and I see no reason to hold up a genuinely useful feature like designated initailizers just because it doesn't cover a particular case as nicely as you would prefer.

Thiago Macieira

unread,
Nov 23, 2015, 11:54:19 PM11/23/15
to std-pr...@isocpp.org
On Monday 23 November 2015 19:47:38 Nicol Bolas wrote:
> > They're against making the ugly parameters currently used for various
> > reasons
> > become API.
>
> But as has been previously stated, the standard doesn't define those
> parameter names. So they wouldn't be able to become part of the API. They
> would be implementation defined, rather than standard defined.

The standard doesn't define, but all implementations have parameter names for
at least every parameter that is actually used in inline implementations. If
the proposal for designated initialisers will not require an a specialised
syntax for parameter names, then the existing, ugly names will be available
for library users.

> And I'd point out that that nobody seems to be willing to stop concepts
> from getting into C++17 just because it hasn't been applied to the standard
> library.

That's only analogous to the case where designated initialisers for parameter
names requires a new syntax: until the library implementations "opt in" to the
parameter names, they can't be used.

> But then again, the whole named parameters discussion is off-topic: this is
> about designated initializers, which should not be conflated with named
> parameters. After all, structure member names are already part of a
> struct's API.

I consider this very much on topic. In C++, most useful aggregates have
constructors and unless we come up with a way for designating parameters to
constructors, designated initialisers will have a somewhat limited use for us.

Thiago Macieira

unread,
Nov 24, 2015, 12:11:57 AM11/24/15
to std-pr...@isocpp.org
On Monday 23 November 2015 20:47:47 Nicol Bolas wrote:
> Why not? I already explained how to get the equivalent behavior without
> having actual named parameters (have the type take a struct and use an
> extra set of braces).

It's not as efficient and is much more limited.

Suppose these two constructor examples:

struct S
{
S(int n);
S(int n, std::tuple<std::string, std::vector<int>> data);
};

vs

struct S
{
struct ConstructorArguments {
std::tuple<std::string, std::vector<int>> data;
int n;
};
S(ConstructorArguments);
};

The first thing to note is that we cannot create an overload, as otherwise
that would necessitate an ugly syntax specifying the structure type, as in:
S{(ConstructWithLength){ .n = 1 }};
S((ConstructWithLengthAndData){ .n = 1, .data = {"", {}}};
that is very C-like and C++-unlike.

The second thing is that, since we cannot create overloads, we conclude all
types in the constructor aggregates must be initialised before the constructor
itself is called. As the example shows, this makes it impossible to optimise
for a case where such a parameter isn't needed.

The third thing is that, with the lack of overloads, it's impossible to have
constructors with different behaviours. Think of these std::string
constructors:

basic_string(const value_type* s, size_type n, const allocator_type& a =
allocator_type());
basic_string(size_type n, value_type c, const allocator_type& a =
allocator_type());

The aggregate would need to contain both s and c, so how can the called
constructor know which of the two was actually intended to be used?

Finally, there's also the issue of efficiency for non-inline calls. Take again
the case of std::string with the second constructor above. The size_type and
value_type types are PODs and allocator_type is an empty type, so quite a few
ABIs simply pass the three values in plain registers. If you replace that with
an aggregate, those ABIs will need to spill the primitive values to a
temporary in the stack and pass that temporary's address to the constructor.

The same thing also prevents tail-call optimisations when you need to call a
function with a different object.

Hannes

unread,
Nov 24, 2015, 5:58:00 AM11/24/15
to ISO C++ Standard - Future Proposals
On Tuesday, November 24, 2015 at 6:11:57 AM UTC+1, Thiago Macieira wrote:
On Monday 23 November 2015 20:47:47 Nicol Bolas wrote:
> Why not? I already explained how to get the equivalent behavior without
> having actual named parameters (have the type take a struct and use an
> extra set of braces).

It's not as efficient and is much more limited.

Suppose these two constructor examples:

struct S
{
        S(int n);
        S(int n, std::tuple<std::string, std::vector<int>> data);
};

vs

struct S
{
        struct ConstructorArguments {
                std::tuple<std::string, std::vector<int>> data;
                int n;
        };
        S(ConstructorArguments);
};

The first thing to note is that we cannot create an overload, as otherwise
that would necessitate an ugly syntax specifying the structure type, as in:
        S{(ConstructWithLength){ .n = 1 }};
        S((ConstructWithLengthAndData){ .n = 1, .data = {"", {}}};
that is very C-like and C++-unlike.

I just noted that anonymous struct declaration is currently not allowed by the language as parameter types. The proposal could allow them in the specific case when you have constructors that only accept them in a single argument. A designated aggregate initialization would only be able to call this new allowed parameter form to prevent name leakage:

struct S {
    // Constructor 1
    S(struct {
        int n;
    });
    // Constructor 2
    S(struct {
        std::tuple<std::string, std::vector<int>> data;
        int n;
    });
};

S v{3}                  // Constructor 1 (classic aggregate initialization)
S v{3, {"foo", {4, 2}}} // Constructor 2 (classic aggregate initialization)

S v{n: 3}                  // Constructor 1 (designated aggregate initialization)
S v{n: 3, {"foo", {4, 2}}} // Constructor 2 (designated aggregate initialization)

S v(3)                  // Constructor 1 (backwards-compatible)
S v(3, {"foo", {4, 2}}) // Constructor 2 (backwards-compatible)


The syntax would be completely analogous to having normal parameter lists in terms of overloading priority. The only "quirk" would be the fact that it wouldn't be a true anonymous struct because the constructor declaration would be considered equivalent as long as it had the same members in the anonymous struct with the same types but that would still be analogous with normal parameter lists.

This would also solve your second and third concern (performance and different behaviours). This could be argued to be a completely separate feature though but it would be introduced at the same time to allow the benefit of designated initilization in higher C++ as well.
 
Finally, there's also the issue of efficiency for non-inline calls. Take again
the case of std::string with the second constructor above. The size_type and
value_type types are PODs and allocator_type is an empty type, so quite a few
ABIs simply pass the three values in plain registers. If you replace that with
an aggregate, those ABIs will need to spill the primitive values to a
temporary in the stack and pass that temporary's address to the constructor.

Are those ABIs major ABIs? You make quite a few assumptions here about modern ABIs. Just because you wrap a value in a POD doesn't mean it spills to the stack.

Code (C):

typedef struct {
int a;
  int b;
} t;

t foo() {
  return (t) {.a = 1, .b = 2};
}

x86: (no stack)

foo():
movabsq $8589934593, %rax
ret

ARM64: (no stack)

foo():
mov x0, 0
mov x1, 1
bfi x0, x1, 0, 32
mov x1, 2
bfi x0, x1, 32, 32
ret

ARM: (no stack)

foo():
mov r2, r0
movw r3, #:lower16:.LANCHOR0
movt r3, #:upper16:.LANCHOR0
ldmia r3, {r0, r1}
stmia r2, {r0, r1}
mov r0, r2
bx lr

The same thing also prevents tail-call optimisations when you need to call a
function with a different object.

How? The parameters you call a function with is irrelevant for TCO, even if they spill onto the stack or change. The important part is that you return the same type so that you can just overwrite the return pointer.

Hannes

unread,
Nov 24, 2015, 6:00:16 AM11/24/15
to ISO C++ Standard - Future Proposals
Oops, bogus example, constructor 2 should be:

    // Constructor 2
    S(struct {
        int n;
        std::tuple<std::string, std::vector<int>> data;
    });

Nicol Bolas

unread,
Nov 24, 2015, 10:12:01 AM11/24/15
to ISO C++ Standard - Future Proposals
On Tuesday, November 24, 2015 at 12:11:57 AM UTC-5, Thiago Macieira wrote:
On Monday 23 November 2015 20:47:47 Nicol Bolas wrote:
> Why not? I already explained how to get the equivalent behavior without
> having actual named parameters (have the type take a struct and use an
> extra set of braces).

It's not as efficient and is much more limited.

... so?

Designated initializers exist to make aggregates work better and nicer. They are not a way to back-door named parameters into the language, nor are they an attempt to make constructors work with them.

The fact that they don't solve something that they're not actually supposed to solve is not a problem with the feature. It's a problem with your expectations and desires projected onto the feature.

Thiago Macieira

unread,
Nov 24, 2015, 10:22:30 AM11/24/15
to std-pr...@isocpp.org
On Tuesday 24 November 2015 07:12:01 Nicol Bolas wrote:
> On Tuesday, November 24, 2015 at 12:11:57 AM UTC-5, Thiago Macieira wrote:
> > On Monday 23 November 2015 20:47:47 Nicol Bolas wrote:
> > > Why not? I already explained how to get the equivalent behavior without
> > > having actual named parameters (have the type take a struct and use an
> > > extra set of braces).
> >
> > It's not as efficient and is much more limited.
>
> ... so?
>
> Designated initializers exist to make aggregates work better and nicer.
> They are *not* a way to back-door named parameters into the language, nor
> are they an attempt to make constructors work with them.
>
> The fact that they don't solve something that they're not actually supposed
> to solve is *not* a problem with the feature. It's a problem with your
> expectations and desires projected onto the feature.

I'm merely pointing out that you can't say that designated initialisers are a
replacement for function parameter initialisers. They are not.

Thiago Macieira

unread,
Nov 24, 2015, 10:38:58 AM11/24/15
to std-pr...@isocpp.org, Hannes
On Tuesday 24 November 2015 02:58:00 Hannes wrote:
> > struct S
> > {
> > struct ConstructorArguments {
> > std::tuple<std::string, std::vector<int>> data;
> > int n;
> > };
> > S(ConstructorArguments);
> > };
> >
> > The first thing to note is that we cannot create an overload, as otherwise
> > that would necessitate an ugly syntax specifying the structure type, as
> >
> > in:
> > S{(ConstructWithLength){ .n = 1 }};
> > S((ConstructWithLengthAndData){ .n = 1, .data = {"", {}}};
> >
> > that is very C-like and C++-unlike.
>
> I just noted that anonymous struct declaration is currently not allowed by
> the language as parameter types. The proposal could allow them in the
> specific case when you have constructors that only accept them in a single
> argument.

I don't think anonymous structs are required. The point is that you should
never have to name them, so whether they are anonymous or not is entirely
orthogonal to the issue.

That said, I don't think your solution will work:

> A designated aggregate initialization would only be able to call
> this new allowed parameter form to prevent name leakage:
>
> struct S {
> // Constructor 1
> S(struct {
> int n;
> });
> // Constructor 2
> S(struct {
// corrected:
> int n;
> std::tuple<std::string, std::vector<int>> data;
> });
> };
>
> S v{3} // Constructor 1 (classic aggregate initialization)
> S v{3, {"foo", {4, 2}}} // Constructor 2 (classic aggregate initialization)
>
> S v{n: 3} // Constructor 1 (designated aggregate
> initialization)
> S v{n: 3, {"foo", {4, 2}}} // Constructor 2 (designated aggregate
> initialization)
>
> S v(3) // Constructor 1 (backwards-compatible)
> S v(3, {"foo", {4, 2}}) // Constructor 2 (backwards-compatible)

Doesn't all first entries in each pair cause an ambiguous overload resolution?
The parameter n appears in both structures and both structures have an
integral as their first argument.

> > Finally, there's also the issue of efficiency for non-inline calls. Take
> > again
> > the case of std::string with the second constructor above. The size_type
> > and
> > value_type types are PODs and allocator_type is an empty type, so quite a
> > few
> > ABIs simply pass the three values in plain registers. If you replace that
> > with
> > an aggregate, those ABIs will need to spill the primitive values to a
> > temporary in the stack and pass that temporary's address to the
> > constructor.
>
> Are those ABIs major ABIs? You make quite a few assumptions here about
> modern ABIs. Just because you wrap a value in a POD doesn't mean it spills
> to the stack.

Try with three parameters instead of two. Try with any non-trivial type too.

And even with two, like in your example, note how the two ints got compressed
into one register, instead of being each passed in a register. This means the
callee has a more complex code path to work with each.

> > The same thing also prevents tail-call optimisations when you need to call
> > a
> > function with a different object.
>
> How? The parameters you call a function with is irrelevant for TCO, even if
> they spill onto the stack or change. The important part is that you return
> the same type so that you can just overwrite the return pointer.

TCO requires that the callee can pop the stack of the caller. That cannot
happen if the caller had to allocate space for the parameters on the stack.

struct S { int a, b, c; };
void f(S);
void g(int a, int b, int c)
{
f({a, b, c});
}

The above on x86-64 requires spilling to the stack.

The point is simply that using a struct in lieu of actual parameters is not
guaranteed to have the same ABI calling conventions, which may prevent TCO
from happening and quite often will have worse performance for one reason or
another.

Nicol Bolas

unread,
Nov 24, 2015, 11:07:15 AM11/24/15
to ISO C++ Standard - Future Proposals
On Monday, November 23, 2015 at 11:54:19 PM UTC-5, Thiago Macieira wrote:
On Monday 23 November 2015 19:47:38 Nicol Bolas wrote:
> > They're against making the ugly parameters currently used for various
> > reasons
> > become API.
>
> But as has been previously stated, the standard doesn't define those
> parameter names. So they wouldn't be able to become part of the API. They
> would be implementation defined, rather than standard defined.

The standard doesn't define, but all implementations have parameter names for
at least every parameter that is actually used in inline implementations. If
the proposal for designated initialisers will not require an a specialised
syntax for parameter names, then the existing, ugly names will be available
for library users.

I'm going to pretend you said "named parameters" instead of "designated initializers", since they're two different features, no matter how much you wish to conflate them.

Implementations often export symbols that they don't intend for users to use. You can find things in `std::__detail` namespaces or whatever that you could start making your program rely on.

That doesn't make it standard-protected behavior for you to rely on them.

If a user starts using parameter names that are not defined by the standard, then it's no different from them poking at things in the `std::__detail` namespace. In both cases, they are relying on implementation-defined behavior.
 
> And I'd point out that that nobody seems to be willing to stop concepts
> from getting into C++17 just because it hasn't been applied to the standard
> library.

That's only analogous to the case where designated initialisers for parameter
names requires a new syntax: until the library implementations "opt in" to the
parameter names, they can't be used.

If the names aren't defined by the standard, then you're not allowed to rely on them, per the above.
 
> But then again, the whole named parameters discussion is off-topic: this is
> about designated initializers, which should not be conflated with named
> parameters. After all, structure member names are already part of a
> struct's API.

I consider this very much on topic.

I can't stop you from considering it whatever you like. But as far as designated initializers as a feature is concerned, it is only about aggregate initialization. It has nothing to do with function parameter names and named parameters.
 
In C++, most useful aggregates have
constructors

No useful aggregates with constructors exist because, by C++'s definition, an aggregate cannot have a constructor.

However, I'll pretend you used the right terminology there (classes, not aggregates). The Core C++ Guidelines seem to disagree with you. Constructors exist to protect invariants. And not all compilations of types need invariants. Indeed, the inference from the guidelines is that you are intended to write types that have the minimal invariant possible. Which means that, if you have a type that contains two types, but doesn't have any invariant between them, then what you have is a struct that has two types.

An aggregate.
 
and unless we come up with a way for designating parameters to
constructors, designated initialisers will have a somewhat limited use for us.

Just because it doesn't solve every problem doesn't mean we shouldn't have it. It solve actual, useful problems for people as is.

Hannes

unread,
Nov 24, 2015, 12:13:27 PM11/24/15
to ISO C++ Standard - Future Proposals, land...@gmail.com

On Tuesday, November 24, 2015 at 4:38:58 PM UTC+1, Thiago Macieira wrote:
I don't think anonymous structs are required. The point is that you should
never have to name them, so whether they are anonymous or not is entirely
orthogonal to the issue.

It solves the problem where the library would be forced to expose a new standardized aggregate type name in the API to application code. Allowing anonymous structs would allow the library to force the application code to use unnamed initializer lists.

 
Doesn't all first entries in each pair cause an ambiguous overload resolution?
The parameter n appears in both structures and both structures have an
integral as their first argument.

Yes, this is currently ambiguous with aggregate initialization. However the idea was to introduce more overloading priority rules to make it analogous to parameter overloading, e.g. select the anonymous struct with the fewest unspecified/default fields.

That said the anonymous struct thing is just a silly idea I had. Personally I completely agree with Nicol that designated initializers should just be considered an extension to aggregate initialization and has nothing to do with this named parameter business.

TCO requires that the callee can pop the stack of the caller. That cannot
happen if the caller had to allocate space for the parameters on the stack.

You're right, I made a premature assumption that the memory was owned by the callee but I now realize that would always force memory copy and thus be inefficient.


The point is simply that using a struct in lieu of actual parameters is not
guaranteed to have the same ABI calling conventions, which may prevent TCO
from happening and quite often will have worse performance for one reason or
another.

This is getting really off-topic now IMO but my counterpoint to that would be that if someone is concerned with the performance loss from some stack spillage they should consider link-time optimization anyway which makes the ABI irrelevant.

Ville Voutilainen

unread,
Nov 24, 2015, 3:05:41 PM11/24/15
to ISO C++ Standard - Future Proposals
On 24 November 2015 at 05:47, Nicol Bolas <jmck...@gmail.com> wrote:
> And I'd point out that that nobody seems to be willing to stop concepts from
> getting into C++17 just because it hasn't been applied to the standard
> library.

You have a funny definition of "nobody". It seems a definition so anecdotal that
it defeats the meaning of the word.

Thiago Macieira

unread,
Nov 24, 2015, 4:58:12 PM11/24/15
to std-pr...@isocpp.org
On Tuesday 24 November 2015 09:13:27 Hannes wrote:
> That said the anonymous struct thing is just a silly idea I had. Personally
> I completely agree with Nicol that designated initializers should just be
> considered an extension to aggregate initialization and has nothing to do
> with this named parameter business.

I'm not disputing that.

I'm simply saying that it will have limited use because the interesting
aggregates in C++ have non-trivial default constructors, which removes the
ability to use designated initialisers.

If, however, designated parameters were implemented, it would apply to
constructors too and seamlessly remove the limitation.

Thiago Macieira

unread,
Nov 24, 2015, 5:15:43 PM11/24/15
to std-pr...@isocpp.org
On Tuesday 24 November 2015 08:07:14 Nicol Bolas wrote:
> I'm going to pretend you said "named parameters" instead of "designated
> initializers", since they're *two different features*, no matter how much
> you wish to conflate them.

Yes, I used the wrong term there.

> Implementations often export symbols that they don't intend for users to
> use. You can find things in `std::__detail` namespaces or whatever that you
> could start making your program rely on.
>
> That doesn't make it standard-protected behavior for you to rely on them.

I agree with you that people shouldn't rely on them, but it may still be the
fear of library developers that users may start depending on unspecified parts
of the API.

> > In C++, most useful aggregates have
> > constructors
>
> No useful aggregates with constructors exist because, by C++'s definition,
> an aggregate cannot have a constructor
> <http://en.cppreference.com/w/cpp/language/aggregate_initialization>.

Sorry, I misused the term. I wanted to use something that encompassed both
"class" and "struct", so I opted for "aggregate", which does have the meaning
I meant in C and includes the other type of designated initialisers we're
talking about. In C11 6.2.5 Types, paragraph 21:

Arithmetic types and pointer types are collectively called scalar types.
Array and structure types are collectively called aggregate types.

Obviously C doesn't need the distinction that dcl.init.aggr p1 makes, since C
has no constructor or access level. I had just never realised C++ had the
distinction.

> However, I'll pretend you used the right terminology there (classes, not
> aggregates). The Core C++ Guidelines seem to disagree with you.
> Constructors exist to protect invariants.

The guidelines are irrelevant to me, since they do not apply to the library I
mostly work on.

> > and unless we come up with a way for designating parameters to
> > constructors, designated initialisers will have a somewhat limited use for
> > us.
>
> Just because it doesn't solve every problem doesn't mean we shouldn't have
> it. It solve actual, useful problems for people as is.

Not disputing that it solves problems. I'm saying simply that I wish we went
further and enabled a whole set of new and more common uses.
Reply all
Reply to author
Forward
0 new messages