Named function parameters using anonymous structs for C and C++

1,518 views
Skip to first unread message

Matthew Fioravante

unread,
Jun 15, 2014, 2:17:25 PM6/15/14
to std-pr...@isocpp.org
A possible solution to implementing named function parameters.

//declaration
//throws on error
void display(Screen& target, { int xres, int yres/*, etc..*/ }); //<- compiler generated anonymous struct type

//definition
void display(Screen& target, {} p) { //<- p derives anonymous struct type from declaration. Error if no declaration present.
doDisplayWork(target, p.xres, p.yres);
}

//At call site
Screen screen;
display(screen, {}); //<-defaults
display(screen); //<-has a default set to {}, works like positional default args
display(screen, 640, 480); 
display(screen, yres=480, xres=640);
display(screen, { yres=480, xres=640});


This might have better support in ABI. The type of the function is derived from the type of the anonymous struct. This could be exploited to enhance error checking in the compiler.

A tool in clang could probably be written to convert legacy interfaces on large C++ code bases to decide how to use the new style on a case by case basis.

This idea could be implemented in both C and C++.


Extensions:
Multiple struct argument packs
int something(T pos0, U pos1, {}, {});

Low level compiler generated abstraction around the function call frame __args.
int something(T pos0, U pos1, {} q, {} r) {
  assert(&pos0 == &__args.pos[0]);
  assert(&pos1 == &__args.pos[1]);
  __args.pos[2]; //<-compiler error
  __args.q.somearg;
  __args.r.somearg;
  __args.returnaddr; //int*, the return address. Would this be useful to have? It could be abused to violate the type system by using it before the return value has been constructed. It could be hooked into a memory allocator strategy to use a specific memory strategy for return values. 
  __args.return; //int&, reference to the return value.
}

Anonymous struct lambda. Is this useful?
Enable this syntax:
auto h = { int x; int y; };

 

Message has been deleted

walter1234

unread,
Jun 15, 2014, 5:06:23 PM6/15/14
to std-pr...@isocpp.org
I agree anonymous structs would be a great feature.

[1] Would there be less risk of ambiguous parses if the designated initialiser syntax of C was taken  - and would that make it easier to retrofit into C? 

  void display(Screen& target, struct { int xres, int yres/*, etc..*/ }); //<- compiler generated anonymous struct type

  display(screen, { .yres=480, .xres=640});

[2] Would it also be a good way of doing multiple return values. Might actually be more pleasant than tuples in other languages.

   auto do_something()-> struct { int x; int y  }

[3] could it work with auto...

   auto do_something() {
       return {.x=..., .y=....}
   }

Matthew Fioravante

unread,
Jun 16, 2014, 3:18:52 PM6/16/14
to std-pr...@isocpp.org


On Sunday, June 15, 2014 5:06:23 PM UTC-4, walter1234 wrote:
I agree anonymous structs would be a great feature.

[1] Would there be less risk of ambiguous parses if the designated initialiser syntax of C was taken  - and would that make it easier to retrofit into C? 

  void display(Screen& target, struct { int xres, int yres/*, etc..*/ }); //<- compiler generated anonymous struct type

  display(screen, { .yres=480, .xres=640});

Yes, I think reusing the C syntax makes the most sense here.

That also implies adding support for C tagged structure initialization to C++, which implies defining how it works with constructors. At first pass, I'd say only enable the syntax for POD structs. This would need to be studied very carefully. I wouldn't be surprised if this was already brought up in the past, studied, and rejected for some reason. Does anyone have insight on that? Google doesn't appear to be helpful.
 

[2] Would it also be a good way of doing multiple return values. Might actually be more pleasant than tuples in other languages.

   auto do_something()-> struct { int x; int y  }

I did not think of that, but this is not a bad idea. The multiple return values have names which makes the code much more understandable than using tuples as you pointed out.
 

[3] could it work with auto...

   auto do_something() {
       return {.x=..., .y=....}
   }


Sure, as long as the parser can differentiate between an ordinary "tagged initialization" expression and this expression which is an "anonymous type declaration and tagged initialization" expression all in one.

It looks like this idea is really multiple proposals in one:

1. Add support for tagged struct initialization syntax for POD structs to C++
2. Add support for anonymous structs
3. Add syntactic sugar (may not even be necessary) for function declarations and invocations to use anonymous structs to support named function arguments.
4. Implicit struct for call stack reflection? (May be an entirely different idea to be discussed separately).


Can anyone suggest why this might be a terrible idea to pursue?

Thiago Macieira

unread,
Jun 16, 2014, 3:33:32 PM6/16/14
to std-pr...@isocpp.org
Em seg 16 jun 2014, às 12:18:51, Matthew Fioravante escreveu:
> That also implies adding support for C tagged structure initialization to
> C++, which implies defining how it works with constructors. At first pass,
> I'd say only enable the syntax for POD structs. This would need to be
> studied very carefully. I wouldn't be surprised if this was already brought
> up in the past, studied, and rejected for some reason. Does anyone have
> insight on that? Google doesn't appear to be helpful.
>

The interaction with constructors could be just the same as named arguments to
the constructor.

Examples:

struct NoConstructor
{
int x;
int y;
};
NoConstructor nc { .x = 1, .y = 2 };

class Constructor
{
int m_x;
int m_y;
public:
constexpr Constructor(int x, int y) : m_x(x), m_y(y) {}
};
Constructor c { .x = 1, .y = 2 };

In other words, an aggregate with no constructors declared is equivalent to
having constructors taking the exact same types as the members, with the same
names, with every argument having as default the default construction of the
type in question.

--
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

Matthew Fioravante

unread,
Jun 16, 2014, 4:14:42 PM6/16/14
to std-pr...@isocpp.org

I agree, and actually this would be useful in other areas because many times I've been forced to write a stupid forwarding constructor for struct types which I used internally to implement classes. One major advantage of these compiler generated forwarding constructors is that it can handle perfect forwarding exactly for all combinations of lvalues and rvalues.

What we're essentially doing is adding a compiler generated default constructor which takes a set of arguments equal to the data members. This does not make sense at all unless all data members are public.

Now we have opened pandora's box. If we take this step, a lot of questions follow:

Do we need to support = default?

struct S {
  int x;
  int y;
  S(int x, int y) = default;
}
How do we "default" all of the lvalue/rvalue variations?

We probably also need = delete, if people want to disable this behavior.

What about inheritance? The below seems reasonable:

struct A {
  int a;
  int b;
};
 
struct B : public A {
  int c;
  int d;
};

B b{ .a = 1, .b = 2, .c =3, .d = 4};

How about copy initialization?

B b = { a. =1 , b = .2, .c = 3, .d = 4};

How about assignment?

B b;
b = { a. = 1, ... };

If the {} expression produces an anonymous struct, that means we need some kind of implicit conversion to B. How does this affect C compatibility?

How about unions?


A struct in this sense is really just a tuple with names, in some cases more flexible than a tuple because the compiler is free to pad the members however it likes. For example with the idea of an implicit struct wrapping the call stack, the compiler can add padding for stack canaries and any other implementation specific bits and still be compatible within the rules of a C++ struct. You can compose structs with inheritance. You can selectively initialize them with the tagged syntax. You can pass them around and their data members get copied/moved as necessary. We can call it struct-ured programming (new buzzword!).

This would take a lot of effort to iron out all of the little details.

corentin....@cea.fr

unread,
Jun 16, 2014, 5:43:57 PM6/16/14
to std-pr...@isocpp.org
Le lundi 16 juin 2014 21:18:52 UTC+2, Matthew Fioravante a écrit :
That also implies adding support for C tagged structure initialization to C++, which implies defining how it works with constructors. At first pass, I'd say only enable the syntax for POD structs. This would need to be studied very carefully. I wouldn't be surprised if this was already brought up in the past, studied, and rejected for some reason. Does anyone have insight on that? Google doesn't appear to be helpful.

A proposal has been written recently:
https://groups.google.com/a/isocpp.org/forum/#!msg/std-proposals/IgDFqKjKlRs/CGARpDJy9JsJ
I do not know what is the current status of that proposal though.

Thiago Macieira

unread,
Jun 16, 2014, 5:51:31 PM6/16/14
to std-pr...@isocpp.org
Em seg 16 jun 2014, às 13:14:42, Matthew Fioravante escreveu:
> I agree, and actually this would be useful in other areas because many
> times I've been forced to write a stupid forwarding constructor for struct
> types which I used internally to implement classes. One major advantage of
> these compiler generated forwarding constructors is that it can handle
> perfect forwarding exactly for all combinations of lvalues and rvalues.

It's also a trivial constructor if the types are trivial, constexpr if the
types are literal, etc.

> What we're essentially doing is adding a compiler generated default
> constructor which takes a set of arguments equal to the data members. This
> does not make sense at all unless all data members are public.

Correct, but that doesn't change brace initialisation. You can't do that for
non-public members:

<stdin>:1:26: error: no matching constructor for initialization of 'X'
class X { int x, y; }; X x = { 1 };
[...]
<stdin>:1:7: note: candidate constructor (the implicit default constructor)
not viable: requires 0 arguments, but 1 was provided

> Now we have opened pandora's box. If we take this step, a lot of questions
> follow:
>
> Do we need to support = default?
>
> struct S {
> int x;
> int y;
> S(int x, int y) = default;
> }
> How do we "default" all of the lvalue/rvalue variations?

Hmm... that's a good question. Maybe if you default the default constructor,
it should just work. GCC, Clang and ICC appear to already work like that.

struct S {
int x;
int y;
S(const S &) = delete;
S() = default;
};
S s { 1 };

This 1-argument constructing is matching the implicit default constructor. If
you remove the "= default" part, then the compiler complains that it can't
match to any constructor.

> We probably also need = delete, if people want to disable this behavior.

As above: just delete the default constructor.

> What about inheritance? The below seems reasonable:
>
> struct A {
> int a;
> int b;
> };
>
> struct B : public A {
> int c;
> int d;
> };
>
> B b{ .a = 1, .b = 2, .c =3, .d = 4};

This does not exist in C, so we're in new territory here. In C++98, B wasn't
eligible for brace initialisation because it wasn't POD. With C++11, it's now
trivial layout and trivially initialisable, but it's still not brace-
initialisable.

I don't see a need to change. You must provide a constructor if you want the
above to work:

constexpr B(int a, int b, int c, int d) : A{a, b}, c{c}, d{d} {}

> How about copy initialization?
>
> B b = { a. =1 , b = .2, .c = 3, .d = 4};

Same rules as from pre-history: the copy constructor is elided and you just
call the default constructor above. It needs to be supported for C99
compatibility too.

> How about assignment?
>
> B b;
> b = { a. = 1, ... };

This is a different one. It probably should not work as written. But this
should:

B b;
b = B{ .a = 1 };
b = B(.a = 1);

> If the {} expression produces an anonymous struct, that means we need some
> kind of implicit conversion to B. How does this affect C compatibility?
>
> How about unions?

You can initialise at most one element. Right now, for:

union U { int i; double d; };

The following works:
U u1 = {};
U u2 = { 1 };

But this doesn't:
U u3 = { 1.0 };
due to narrowing of double to int, when trying to initialise U(int) [it works
in C++98 and in C, but produces possibly unexpected results]. This works in
C99:

union U u4 = { .d = 1.0 };

So we should allow in C++ too. So the rule for unions is that the default
constructor is equivalent to one-argument constructors taking each of the
members, with the exact same access level, type and name.

The interesting thing here will be dealing with C++11's unrestricted unions:
the constructor gets deleted by default if any of the members has a non-
trivial constructor (e.g., add std::string s; to the union above). We can
bring them back:

union U {
int i;
std::string s;
constexpr U(int i) : i(i) {}
U(std::string s) : s(s) {}
~U() {}
};

Should there be a shortcut that allows us to get the per-element constructors?
By the way, if you write ~U() = default; the destructor is still deleted.

Matthew Fioravante

unread,
Jun 16, 2014, 8:36:59 PM6/16/14
to std-pr...@isocpp.org


On Monday, June 16, 2014 5:51:31 PM UTC-4, Thiago Macieira wrote:
> What we're essentially doing is adding a compiler generated default
> constructor which takes a set of arguments equal to the data members. This
> does not make sense at all unless all data members are public.

Correct, but that doesn't change brace initialisation. You can't do that for
non-public members:

Actually now that I think about it that doesn't make sense. Braced initialization is not creating new constructors. Its just another C compatible magic abomination which lets you initialize structs. For C compatibility, tagged initialization syntax should work the same way as braced initialization syntax, and Daryle's proposal seems to be providing just that.

Now we are discussing something entirely different. That is compiler generated default constructors for structs which have a parameter list matching the data members. In this case, initializing the data members by name will depend on a named function argument proposal, either the idea sketched earlier here or another implementation.

We already get this from C and Daryle's proposal:
struct S { int x; int y; };
1) S s{1, 2};
2) S s{.y = 2};

Adding actual constructors to the mix, gives us these:
3) S s(1, 2);
4) S s(.y = 2); //Depends on a named function arguments proposal

std::vector<S> s;
5) s.emplace_back(1, 2);
6) s.emplace_back(.y = 2); //This would be nice

I think the emplace_back() example (5) here is a real issue. We should be able to perfect forward the struct initialization syntax for both tagged and non-tagged braced initialization. Having an actual constructor for S generated by the compiler would allow perfect forwarding to work for (5) and also give us (3) . Adding named function parameters on top of that would enable (4) and (6).

By adding the compiler generated member constructor, we get perfect forwarding to work here in emplace_back and other forwarding functions which I believe is a good thing. Now there is a complication where when you initialize a struct, sometimes you're doing this magic C brace thing, and other times you're calling a constructor.

S s{1, 2}; //C brace init magic thing
S s(1, 2); //Constructor call
v.emplace_back(1, 2); //Constructor call

BIG QUESTION: Does this difference actually mean anything in practice anywhere? If it does that's another stupid esoteric C++ rule that people have to learn. That's a big problem. I hate those with a passion and would really not like to introduce more.

Some solutions to this:
1) Do nothing. If the semantics are exactly the same, and only language lawyers need to know the difference then its probably fine.
2) Change {} initialization to call the constructor. Actually this can be accomplished by simply removing the C brace initialization rules from the standard. The syntax would then "fall back" to just being a constructor call. For all purposes of C compatibility, the semantics would have to be the same. This would also mean we don't need Daryle's proposal at all, because named function parameters would also give us the tagged brace initialization syntax for free.


>
> struct S {
>   int x;
>   int y;
>   S(int x, int y) = default;
> }
> How do we "default" all of the lvalue/rvalue variations?

Hmm... that's a good question. Maybe if you default the default constructor,
it should just work. GCC, Clang and ICC appear to already work like that.

It looks like on gcc 4.9 you can still use brace and tagged initialization even if you delete the default constructor. Its only when you define another constructor that braced initialization gets disabled. Was that an oversight in C++11? Tieing C compatible braced and tagged initialization to the default constructor seems like a better idea than depending on whether a user defined constructor was written.

If I delete the default constructor, I should disable brace init as well. Removing brace init in favor of implict constructors would fix this problem as well.


> What about inheritance? The below seems reasonable:
>
> struct A {
>   int a;
>   int b;
> };
>
> struct B : public A {
>   int c;
>   int d;
> };
>
> B b{ .a = 1, .b = 2, .c =3, .d = 4};

This does not exist in C, so we're in new territory here. In C++98, B wasn't
eligible for brace initialisation because it wasn't POD. With C++11, it's now
trivial layout and trivially initialisable, but it's still not brace-
initialisable.

I don't see a need to change. You must provide a constructor if you want the
above to work:

        constexpr B(int a, int b, int c, int d) : A{a, b}, c{c}, d{d} {}

I suppose that's fair. If you want to compose structs you can just use containment instead of inheritance. Containment also avoids name clashes if you have multiple inheritance and 2 base classes with the same data member names.
 

> How about copy initialization?
>
> B b = { a. =1 , b = .2, .c = 3, .d = 4};

Same rules as from pre-history: the copy constructor is elided and you just
call the default constructor above. It needs to be supported for C99
compatibility too.

> How about assignment?
>
> B b;
> b = { a. = 1, ... };

This is a different one. It probably should not work as written. But this
should:

        B b;
        b = B{ .a = 1 };

Seems ok, if tagged initialization is not actually defining a new constructor and works like brace initialization.
 
        b = B(.a = 1);

This should fail to compile, unless we have these implicit constructors and named constructor arguments.
 

> If the {} expression produces an anonymous struct, that means we need some
> kind of implicit conversion to B. How does this affect C compatibility?
>
> How about unions?

You can initialise at most one element. Right now, for:

union U { int i; double d; };

The following works:
        U u1 = {};
        U u2 = { 1 };

But this doesn't:
        U u3 = { 1.0 };
due to narrowing of double to int, when trying to initialise U(int) [it works
in C++98 and in C, but produces possibly unexpected results]. This works in
C99:

        union U u4 = { .d = 1.0 };
 
So we should allow in C++ too.

Pretty straightforward for the C tagged syntax if that is to be adopted.

So the rule for unions is that the default
constructor is equivalent to one-argument constructors taking each of the
members, with the exact same access level, type and name.

The interesting thing here will be dealing with C++11's unrestricted unions:
the constructor gets deleted by default if any of the members has a non-
trivial constructor (e.g., add std::string s; to the union above). We can
bring them back:

union U {
        int i;
        std::string s;
        constexpr U(int i) : i(i) {}
        U(std::string s) : s(s) {}
        ~U() {}
};

Unions are an interesting case here.

I think disabling the default constructor is somewhat misguided. It seems like a way of promoting encapsulation within the union, that is making union more like a class and more "safe". You can even add member functions to a union, which to me seems almost useless as you don't know which member is active.

The problem is that its still very easy to access an uninitialized object. You cannot encapsulate all of the initialized objects with a union alone because at minimum you need one more piece of external state, that is a tag to tell you which object is active. For that, you have to wrap the union in a class if you want to protect it. Therefore, unions should be thought of more like structs than classes. I wish the standard would stop trying to protect us from ourselves here. If you are using union, you are pointing the loaded gun at your foot and you know it so everyone should get out of the way. The C++03 restrictions were arbitrary and extremely annoying.

The empty constructor is somewhat dangerous, because all it does is leave the entire union in an undefined state. However it could be useful, if we enable the below:

union U {
int x = 2;
int y;
std::string s;
};

U u; //<-default constructor U::U() will initialized x to 2, x is the active object.

union V {
int x = 2;
int y = 3; //compiler error, as before. Multiple fields initialized.
};

union T {
std::string s = {};
std::vector<int> v;
};

T t; //<-Initializes the string member


Now we can also add explicit default constructors for all of the union members and get the brace initialization syntax back again.

One potential ambiguity:

union U {
std::string s;
std::string v;
};

Which one does U::U(std::string) initialize? Maybe it doesn't matter? Since they both occupy the same memory we could just say the first one, or which one is implementation defined. Maybe its a compiler error, but that could break legacy code.

Finally, now we get to tagged initialization. With C tagged init syntax, everything works just fine.

U u { .s = "hello" };

With constructors with named arguments, maybe it doesn't work.

U u(.s = "hello");

How does the above know which single argument constructor to call? Deduce the types from the initializers and look at the names? I think this is a general conflict when combining overloading and named function parameters that needs to be resolved.

 

Should there be a shortcut that allows us to get the per-element constructors?

Not sure what you mean here?
 
By the way, if you write ~U() = default; the destructor is still deleted.


This is fine. I don't think it ever makes sense to write a destructor for a union because you don't know which member to destruct. This probably should stay deleted.
 
--
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


Ok, after all that my head hurts.

Matthew Fioravante

unread,
Jun 16, 2014, 8:45:09 PM6/16/14
to std-pr...@isocpp.org
And one thing I got wrong, named function parameters won't enable this syntax.

v.emplace_back(.y = 2);

I don't think this is actually possible. Unless variadic templates are enhanced somehow with yet another feature to derive names for their variadic arguments.

Thiago Macieira

unread,
Jun 16, 2014, 9:02:38 PM6/16/14
to std-pr...@isocpp.org
Em seg 16 jun 2014, às 17:36:58, Matthew Fioravante escreveu:
> Some solutions to this:
> 1) Do nothing. If the semantics are exactly the same, and only language
> lawyers need to know the difference then its probably fine.
> 2) Change {} initialization to call the constructor. Actually this can be
> accomplished by simply removing the C brace initialization rules from the
> standard. The syntax would then "fall back" to just being a constructor
> call. For all purposes of C compatibility, the semantics would have to be
> the same. This would also mean we don't need Daryle's proposal at all,
> because named function parameters would also give us the tagged brace
> initialization syntax for free.

I think they are basically the same right now and should remain so. But to
simplify the language rules, we may want to switch to #2.

David Krauss

unread,
Jun 16, 2014, 9:38:27 PM6/16/14
to std-pr...@isocpp.org
It would probably be better to make a clean start, based on review of discussions on this forum.

Note, I was advocating the designated initializer solution earlier in this thread but nobody seemed to care at the time. Anyone who’s interested might check those posts.

I’d also help draft such a proposal if someone wants a collaborator. Right now I’m hoping to attend the November meeting in Champaign, although it’s not a guarantee and I might have a full plate with other proposals. However, at this point a paper exploring the design space might be more appropriate than an attempt at fully-baked wording, which reduces the burden of advocacy. There are a number of issues and corner cases to check, and the committee needs to approve the “what” before the “how.”

David Krauss

unread,
Jun 16, 2014, 9:52:38 PM6/16/14
to std-pr...@isocpp.org
On 2014–06–17, at 9:37 AM, David Krauss <pot...@gmail.com> wrote:


On 2014–06–17, at 5:43 AM, corentin....@cea.fr wrote:

Le lundi 16 juin 2014 21:18:52 UTC+2, Matthew Fioravante a écrit :
That also implies adding support for C tagged structure initialization to C++, which implies defining how it works with constructors. At first pass, I'd say only enable the syntax for POD structs. This would need to be studied very carefully. I wouldn't be surprised if this was already brought up in the past, studied, and rejected for some reason. Does anyone have insight on that? Google doesn't appear to be helpful.

A proposal has been written recently:
https://groups.google.com/a/isocpp.org/forum/#!msg/std-proposals/IgDFqKjKlRs/CGARpDJy9JsJ
I do not know what is the current status of that proposal though.

It would probably be better to make a clean start, based on review of discussions on this forum.

Note, I was advocating the designated initializer solution earlier in this thread but nobody seemed to care at the time. Anyone who’s interested might check those posts.

Oh, it was the other thread. And my strongest point was that, even if named parameters are implemented separately from pass-by-struct, folks will still combine designated initializers with pass-by-struct anyway. This results in two similar ways of doing the same thing, which should have similar if not same syntax.

This thread seems to be simply taking the alternative idea and running with it. :)

One suggestion, but it’s only that: the struct is the sole argument, then allow designated initializer syntax directly in the argument list — i.e., allow brace elision.

struct rect {
    int top, left, bottom, right;

    rect( /* struct */ { int in_top, in_left, in_bottom, in_right; } )
        : top( in_top ), left( in_left ), bottom( in_bottom ), right( in_right ) {}

    rect( /* struct */ { int in_top, in_left, in_height, in_width; } )
        : top( in_top ), left( in_left )
        , bottom( in_top + in_width ), right( in_left + in_width ) {}
};

rect a( .top = 4, .height = 43, .left = 10, .width = 100 );
rect b( .top = 4, .bottom = 47, .left = 10, .right = 110 );


Matthew Fioravante

unread,
Jun 16, 2014, 10:46:10 PM6/16/14
to std-pr...@isocpp.org


On Monday, June 16, 2014 9:52:38 PM UTC-4, David Krauss wrote:

On 2014–06–17, at 9:37 AM, David Krauss <pot...@gmail.com> wrote:


On 2014–06–17, at 5:43 AM, corentin....@cea.fr wrote:

Le lundi 16 juin 2014 21:18:52 UTC+2, Matthew Fioravante a écrit :
That also implies adding support for C tagged structure initialization to C++, which implies defining how it works with constructors. At first pass, I'd say only enable the syntax for POD structs. This would need to be studied very carefully. I wouldn't be surprised if this was already brought up in the past, studied, and rejected for some reason. Does anyone have insight on that? Google doesn't appear to be helpful.

A proposal has been written recently:
https://groups.google.com/a/isocpp.org/forum/#!msg/std-proposals/IgDFqKjKlRs/CGARpDJy9JsJ
I do not know what is the current status of that proposal though.

It would probably be better to make a clean start, based on review of discussions on this forum.

Note, I was advocating the designated initializer solution earlier in this thread but nobody seemed to care at the time. Anyone who’s interested might check those posts.

Oh, it was the other thread. And my strongest point was that, even if named parameters are implemented separately from pass-by-struct, folks will still combine designated initializers with pass-by-struct anyway. This results in two similar ways of doing the same thing, which should have similar if not same syntax.

I completely agree with you that we must reuse designated initializer syntax. Having 2 different syntax just complicates the language. Its completely necessary. There's no bike shed to be painted here!
 
 

This thread seems to be simply taking the alternative idea and running with it. :)

One suggestion, but it’s only that: the struct is the sole argument, then allow designated initializer syntax directly in the argument list — i.e., allow brace elision.

struct rect {
    int top, left, bottom, right;

    rect( /* struct */ { int in_top, in_left, in_bottom, in_right; } )
        : top( in_top ), left( in_left ), bottom( in_bottom ), right( in_right ) {}

    rect( /* struct */ { int in_top, in_left, in_height, in_width; } )
        : top( in_top ), left( in_left )
        , bottom( in_top + in_width ), right( in_left + in_width ) {}
};

rect a( .top = 4, .height = 43, .left = 10, .width = 100 );
rect b( .top = 4, .bottom = 47, .left = 10, .right = 110 );


The original style:

int foo(int a, int b, string c);
foo(.a = 0, .b = 1, .c = "Hello"); 

Pros:
- Very simple
- No ABI changes
- All legacy code immediately becomes named function enabled
Cons:
- Cannot use named syntax when calling via a function pointer, since the type has no concept of parameter names.
- Requires some language changes to enable the callsite syntax
- Argument names while not part of the ABI, are now part of the API. You cannot change the names or you will break callers. 
- All legacy code immediately becomes named function enabled.

The struct approach
int foo(int a, {int b; int c;}, }

Pros:
-Separates positional and named arguments. Is this desirable??
-No ABI changes again, just reuses the type system with anonymous structs
-Function pointers can also be name initialized when called, since their type encodes the type of the struct, and the struct can be initialized using tagged init syntax. This sounds like a big win.
-No special rules, all syntax inherited from struct
Cons;
-Functions with same arguments still have different types.
-Requires anonymous structs proposal
-If you have 2 overloads, with same positional arguments and different structs, how do you choose which one to pick from initializer syntax? 

void foo(int a, {int b; int c;});
void foo(int a, {string b="", string c=""});

foo(1, {.c = "hello" });

I suppose the naming rule here could try to initialize each instance of foo using the brace expression in order of appearance in an SFINAE like manner.


> I’d also help draft such a proposal if someone wants a collaborator. Right now I’m hoping to attend the November meeting > in Champaign, although it’s not a guarantee and I might have a full plate with other proposals. However, at this point a
> paper exploring the design space might be more appropriate than an attempt at fully-baked wording, which reduces the 
> burden of advocacy. There are a number of issues and corner cases to check, and the committee needs to approve the 
> “what” before the “how.”

Does it make sense to cram all of these sort of unrelated things into one big proposal? Or do it incrementally with multiple proposals with the big picture in mind?

I could start writing one or help with writing one and fleshing out the details. It would be great to have someone actually present it at the meetings, since I don't actually have the ability to attend myself.

David Krauss

unread,
Jun 17, 2014, 12:43:28 AM6/17/14
to std-pr...@isocpp.org
On 2014–06–17, at 10:46 AM, Matthew Fioravante <fmatth...@gmail.com> wrote:
 
The original style:

int foo(int a, int b, string c);
foo(.a = 0, .b = 1, .c = "Hello"); 

Pros:
- Very simple
- No ABI changes
- All legacy code immediately becomes named function enabled
Cons:

The big con is that without ABI support, there’s no overloading. My example wouldn’t work because both rect::rect overloads would mangle to the same name.

- Cannot use named syntax when calling via a function pointer, since the type has no concept of parameter names.
- Requires some language changes to enable the callsite syntax
- Argument names while not part of the ABI, are now part of the API. You cannot change the names or you will break callers. 

This is sort-of inherent to the feature, but it suggests that having foreign (member declaration) syntax for named parameters is actually a good thing.

- All legacy code immediately becomes named function enabled.

The struct approach
int foo(int a, {int b; int c;}, }

Pros:
-Separates positional and named arguments. Is this desirable??

It’s desirable that we give some specific meaning to each way an interface may be specified.

My suggestion of brace elision is designed to keep positional and named arguments separate only as long as the interface allows them to be combined.

Partitioning the parameter list into groups sounds like a good idea to me. C99 specifies that undesignated initializers after a designated one initialize successive members. Adding a separator between the named parameters and succeeding unnamed ones prevents initializers from accidentally overflowing.

struct point {
    int top, left;
};

struct rect {
    point top_left, bottom_right;
    float angle;

    rect( /* struct */ { int top, left, bottom, right; }, float in_angle = 0 )
        : top( top ), left( left ), bottom( bottom ), right( right ), angle( in_angle ) {}

    rect( /* struct */ { int top, left, height, width; }, float in_angle = 0 )

        : top( in_top ), left( in_left )
        , bottom( in_top + in_width ), right( in_left + in_width )
        , angle( in_angle ) {}

    rect( /* struct */ { point top_left, point bottom_right; }, float in_angle = 0 )
        : top_left( top_left ), bottom_right( bottom_right ), angle( in_angle ) {}
};

rect a( .top = 4, .height = 43, .left = 10, .width = 100 ); // OK
rect b( .top_left = { 4, 43 }, .bottom_right = { 10, 100 } ); // OK
rect c( .top_left = 4, 43, .bottom_right = 10, 100 ); // OK: (additional) brace elision
rect d( .top = 4, 43, .bottom = 10, 100 ); // OK: initialize successive members.
rect e( .top = 4, 43, 10, 100 ); // Error: ambiguous.
rect f( 4, 43, .bottom = 10, 100 ); // OK, if poor style, the user is asking for it.
rect g( .top = 4, .bottom = 47, .left = 10, .right = 110, 35_degrees ); // Error: mixed named and unnamed
rect h( { .top = 4, .bottom = 47, .left = 10, .right = 110 }, 35_degrees ); // OK


-No ABI changes again, just reuses the type system with anonymous structs
-Function pointers can also be name initialized when called, since their type encodes the type of the struct, and the struct can be initialized using tagged init syntax. This sounds like a big win.
-No special rules, all syntax inherited from struct

I wonder, is dropping the struct keyword really a good idea? The member declaration syntax differs from parameter declarations and it serves as a useful reminder to the author, even if it hinders grokking the interface.

Cons;
-Functions with same arguments still have different types.

Support for taking a function pointer might be an interface feature, requiring the parameter type to get a name.

Given that, a generic facility can create a wrapper that takes arguments according to the function pointer type, and passes them through the named parameter struct type.

-Requires anonymous structs proposal

This is just adopting another feature from C. Extra motivation for cross-compatibility is a good thing.

-If you have 2 overloads, with same positional arguments and different structs, how do you choose which one to pick from initializer syntax? 

void foo(int a, {int b; int c;});
void foo(int a, {string b="", string c=""});

foo(1, {.c = "hello" });

I suppose the naming rule here could try to initialize each instance of foo using the brace expression in order of appearance in an SFINAE like manner.

I don’t see an issue here. Types and names represent independent constraints.

I hadn’t thought about SFINAE or templates, but now that you mention it, named parameters as members cannot be a template parameter deduction context. Might need addressing.

> I’d also help draft such a proposal if someone wants a collaborator. Right now I’m hoping to attend the November meeting > in Champaign, although it’s not a guarantee and I might have a full plate with other proposals. However, at this point a
> paper exploring the design space might be more appropriate than an attempt at fully-baked wording, which reduces the 
> burden of advocacy. There are a number of issues and corner cases to check, and the committee needs to approve the 
> “what” before the “how.”

Does it make sense to cram all of these sort of unrelated things into one big proposal? Or do it incrementally with multiple proposals with the big picture in mind?

I don’t have any real experience yet, but my preference would be to port designated initializers in one paper, anonymous structs in a second, and specify the application to named parameters in a third paper. Ideally, present them simultaneously. However, it’s a lot of ground to cover. Designated initializers will have a lot of interactions with existing initialization features like brace-or-equal-initializers. I think that’s one session, and then anonymous structs can be done in the same session as named parameters, since they’re relatively minor alone. (I don’t even recall whether they have any applications outside unions.)

I could start writing one or help with writing one and fleshing out the details. It would be great to have someone actually present it at the meetings, since I don't actually have the ability to attend myself.

My plans are to write up inline variables, present the preprocessor revision N3881/N3882 which wasn’t presented at Issaquah, and hopefully write-up and present some ideas about scope guards and multithreaded exception handling. So, that’s already a lot.

I think full standardese would be overkill in an initial presentation. The papers would be complete without it. There’s plenty of time before C++17 and we just need to get a foot in the door.

The first thing is to port designated initializers, and the first step there is to try and enumerate all the corner cases. It’s somewhat tangential, but still worthwhile. The C porting work should decide such things as how to provide default arguments to named parameters/members. Ideally the named parameters feature will just magically work when parameters are allowed to contain class-specifiers, so that paper would be the smallest.

Matthew Fioravante

unread,
Jun 17, 2014, 12:48:12 AM6/17/14
to std-pr...@isocpp.org
I'm starting work on a paper for all of these features here:


For now I'm just going to get all of the ideas into one paper with the motivation, problems, details etc.. We can work out some of the details in discussion and if the paper needs to be broken up into multiple papers that can be done later.

I'll update the thread when I have something substantial to share and discuss.

Thiago Macieira

unread,
Jun 17, 2014, 1:10:40 AM6/17/14
to std-pr...@isocpp.org
Em ter 17 jun 2014, às 12:43:19, David Krauss escreveu:
> On 2014-06-17, at 10:46 AM, Matthew Fioravante <fmatth...@gmail.com>
wrote:
> > The original style:
> >
> > int foo(int a, int b, string c);
> > foo(.a = 0, .b = 1, .c = "Hello");
> >
> > Pros:
> > - Very simple
> > - No ABI changes
> > - All legacy code immediately becomes named function enabled
>
> > Cons:
> The big con is that without ABI support, there's no overloading. My example
> wouldn't work because both rect::rect overloads would mangle to the same
> name.

Use a tag struct as a differentiator:

struct TopLeftBottomRight {};
rect(int top, int left, int bottom, int right, TopLeftBottomRight = {})

struct TopLeftWidthHeight {};
rect(int top, int left, int width, int height, TopLeftWidthHeight = {});

> > - Cannot use named syntax when calling via a function pointer, since the
> > type has no concept of parameter names. - Requires some language changes
> > to enable the callsite syntax
> > - Argument names while not part of the ABI, are now part of the API. You
> > cannot change the names or you will break callers.
> This is sort-of inherent to the feature, but it suggests that having foreign
> (member declaration) syntax for named parameters is actually a good thing.

You can specify parameter names in the function pointer syntax...

Matthew Fioravante

unread,
Jun 17, 2014, 1:18:18 AM6/17/14
to std-pr...@isocpp.org


On Tuesday, June 17, 2014 1:10:40 AM UTC-4, Thiago Macieira wrote:
Em ter 17 jun 2014, às 12:43:19, David Krauss escreveu:
> On 2014-06-17, at 10:46 AM, Matthew Fioravante <fmatth...@gmail.com>
wrote:
> > The original style:
> >
> > int foo(int a, int b, string c);
> > foo(.a = 0, .b = 1, .c = "Hello");
> >
> > Pros:
> > - Very simple
> > - No ABI changes
> > - All legacy code immediately becomes named function enabled
>
> > Cons:
> The big con is that without ABI support, there's no overloading. My example
> wouldn't work because both rect::rect overloads would mangle to the same
> name.

Use a tag struct as a differentiator:

        struct TopLeftBottomRight {};
        rect(int top, int left, int bottom, int right, TopLeftBottomRight = {})
        
        struct TopLeftWidthHeight {};
        rect(int top, int left, int width, int height, TopLeftWidthHeight = {});

That's one way to do it, if we cannot come up with a nice naming rule.

> > - Cannot use named syntax when calling via a function pointer, since the
> > type has no concept of parameter names. - Requires some language changes
> > to enable the callsite syntax
> > - Argument names while not part of the ABI, are now part of the API. You
> > cannot change the names or you will break callers.
> This is sort-of inherent to the feature, but it suggests that having foreign
> (member declaration) syntax for named parameters is actually a good thing.

You can specify parameter names in the function pointer syntax...

But they are meaningless, and I don't want to try to propose we change the type system for function pointers. You might be able to extract the names in simple cases, but when it comes to templates, auto, and decltype it could get very messy.

Douglas Boffey

unread,
Jun 17, 2014, 9:43:29 AM6/17/14
to std-pr...@isocpp.org

On Tuesday, 17 June 2014 05:43:28 UTC+1, David Krauss wrote:
 
<snip>
 
struct point {
    int top, left;
};

struct rect {
    point top_left, bottom_right;
    float angle;

    rect( /* struct */ { int top, left, bottom, right; }, float in_angle = 0 )
        : top( top ), left( left ), bottom( bottom ), right( right ), angle( in_angle ) {}

    rect( /* struct */ { int top, left, height, width; }, float in_angle = 0 )
        : top( in_top ), left( in_left )
        , bottom( in_top + in_width ), right( in_left + in_width )
        , angle( in_angle ) {}

    rect( /* struct */ { point top_left, point bottom_right; }, float in_angle = 0 )
        : top_left( top_left ), bottom_right( bottom_right ), angle( in_angle ) {}
};

rect a( .top = 4, .height = 43, .left = 10, .width = 100 ); // OK
rect b( .top_left = { 4, 43 }, .bottom_right = { 10, 100 } ); // OK
rect c( .top_left = 4, 43, .bottom_right = 10, 100 ); // OK: (additional) brace elision
Please, no.  That could only cause confusion, as the number of actual parameters (2) is not the number of apparent parameters (4).

Matthew Fioravante

unread,
Jun 17, 2014, 10:59:09 AM6/17/14
to std-pr...@isocpp.org
As I'm working on the paper and thinking about the details of the proposal, I've found a few issues to wrestle with.

The major difference between aggregate initialization and a set of what I call in the paper compiler generated "aggregate member constructors":

Aggregates can be left in an uninitialized state by not specifying an initializer. If we drop aggregate initialization in favor of constructors, we still need to support this behavior.


Named function parameters using structs chicken and egg problem:

If we had named function params, aka:

int foo(int a, int b, int c);
foo(.a = 1, .b = 2, .c = 4);

Then having designated initializers for structs would just fall out from this in the constructors.

If we introduce designated initializers first, then named function parameters are derived from that. I'm falling more and more into the second camp because as I think about it, separating positional and named arguments seems to make a lot of sense.

The names of the positional arguments stay out of the source level API. If we go with the first approach, all of the sudden the function names everyone is using in legacy code become source level API. This is not something I'd want to wrestle with in a large legacy code base. Instead, we can control our interfaces more explicitly.


David Krauss

unread,
Jun 17, 2014, 11:21:51 AM6/17/14
to std-pr...@isocpp.org
On 2014–06–17, at 10:59 PM, Matthew Fioravante <fmatth...@gmail.com> wrote:

As I'm working on the paper and thinking about the details of the proposal, I've found a few issues to wrestle with.

The major difference between aggregate initialization and a set of what I call in the paper compiler generated "aggregate member constructors":

Aggregates can be left in an uninitialized state by not specifying an initializer. If we drop aggregate initialization in favor of constructors, we still need to support this behavior.

Who is “we”? Surely you don’t mean dropping it from the standard, but you’re just considering the case when the user is migrating a class from from aggregate to constructor?

Hmm. Checking the draft:

First, this syntax is yet another special case inherited for C compatibility. These compatibility cases complicate the standard and the learning curve for C++ developers. It would be better if this feature could be reimplemented using already existing C++ rules and mechanisms and still retain C compatibility.

This seems to be veering away from the problem statement. “Newer would be better” is not much of a motivation. You need to clearly state what’s broken before going down this path. For what it’s worth, aggregate initialization has been maintained just as carefully as the rest of the language.

Named function parameters using structs chicken and egg problem:

If we had named function params, aka:

int foo(int a, int b, int c);
foo(.a = 1, .b = 2, .c = 4);

Then having designated initializers for structs would just fall out from this in the constructors.

If we introduce designated initializers first, then named function parameters are derived from that. I'm falling more and more into the second camp because as I think about it, separating positional and named arguments seems to make a lot of sense.

This seems to be the same as what you’ve been saying throughout this thread. Is there a problem or are you affirming the previous ideas? I’m a little confused. Also I don’t see how this relates to what’s in the posted draft so far.

Matthew Fioravante

unread,
Jun 17, 2014, 11:52:49 AM6/17/14
to std-pr...@isocpp.org


On Tuesday, June 17, 2014 11:21:51 AM UTC-4, David Krauss wrote:

On 2014–06–17, at 10:59 PM, Matthew Fioravante <fmatth...@gmail.com> wrote:

As I'm working on the paper and thinking about the details of the proposal, I've found a few issues to wrestle with.

The major difference between aggregate initialization and a set of what I call in the paper compiler generated "aggregate member constructors":

Aggregates can be left in an uninitialized state by not specifying an initializer. If we drop aggregate initialization in favor of constructors, we still need to support this behavior.

Who is “we”? Surely you don’t mean dropping it from the standard, but you’re just considering the case when the user is migrating a class from from aggregate to constructor?

Hmm. Checking the draft:

First, this syntax is yet another special case inherited for C compatibility. These compatibility cases complicate the standard and the learning curve for C++ developers. It would be better if this feature could be reimplemented using already existing C++ rules and mechanisms and still retain C compatibility.

This seems to be veering away from the problem statement. “Newer would be better” is not much of a motivation. You need to clearly state what’s broken before going down this path. For what it’s worth, aggregate initialization has been maintained just as carefully as the rest of the language.

The idea is that if aggregate initialization for class types can be completely reproduced by compiler generated constructors, we don't actually need it. This is a side question and not too important.

Named function parameters using structs chicken and egg problem:

If we had named function params, aka:

int foo(int a, int b, int c);
foo(.a = 1, .b = 2, .c = 4);

Then having designated initializers for structs would just fall out from this in the constructors.

If we introduce designated initializers first, then named function parameters are derived from that. I'm falling more and more into the second camp because as I think about it, separating positional and named arguments seems to make a lot of sense.

This seems to be the same as what you’ve been saying throughout this thread. Is there a problem or are you affirming the previous ideas? I’m a little confused. Also I don’t see how this relates to what’s in the posted draft so far.


I have not written about named arguments yet in my draft. I'm mostly thinking out loud. It seems like everyone here is in the struct argument passing camp. If anyone has strong opinions either way, I'd love to hear it.
Reply all
Reply to author
Forward
0 new messages