Function Parameter of Struct Type -> Named Parameters

210 views
Skip to first unread message

Andrew Tomazos

unread,
Nov 28, 2018, 11:58:11 PM11/28/18
to std-pr...@isocpp.org
It seems to me that a function that takes a struct combined with C++20 designated initializers gets us very close to named parameters:

struct FooParameters {
    int bar;
    float baz;
};
float foo(FooParameters params) {
   return (params.bar + 3) * params.baz;
}
int main() {
   foo({bar: 4, baz: 3.2});
}

If we added some syntactic sugar that...
1. Removed the need to give the parameter struct type a name
2. Introduced the parameter struct members into the function definition scope.

...we would get pretty close:

float foo(struct { int bar; float baz }) {
    return (bar + 3) * baz;
}
int main() {
   foo({bar: 4, baz:3.2});
}

I'd need to take a look to see if it is ambiguous, but I think we can drop the struct keyword from the above:

float foo({int bar; float baz}) {
    return (bar + 3) * baz;
}
int main() {
   foo({bar: 4, baz:3.2});
}

This would be equivalent to:

struct __A { int bar; float baz };
float foo(__A __a) {
   auto&& [bar,baz] = __a;
    return (bar + 3) * baz;
}
int main() {
   foo({bar: 4, baz:3.2});
}

From a teachability perspective its pretty easy to explain that "a brace-enclosed function parameter is the body of an anonymous struct, the members of which are introduced into the function definition scope."

This also seems easy to implement.

Thoughts?  Worth pursuing?

Magnus Fromreide

unread,
Nov 29, 2018, 1:05:28 AM11/29/18
to std-pr...@isocpp.org
On Thu, Nov 29, 2018 at 02:57:55PM +1000, Andrew Tomazos wrote:
> It seems to me that a function that takes a struct combined with C++20
> designated initializers gets us very close to named parameters:
>
> struct FooParameters {
> int bar;
> float baz;
> };
> float foo(FooParameters params) {
> return (params.bar + 3) * params.baz;
> }
> int main() {
> foo({bar: 4, baz: 3.2});
> }
>
> If we added some syntactic sugar that...
> 1. Removed the need to give the parameter struct type a name
> 2. Introduced the parameter struct members into the function definition
> scope.

I think this misses a lot of benefits and would propose two other rules:

1. Allow anonymous structs
Similar to anonymous unions.
This is already done by VC++ and as an extension by g++ and clang++.
2. Implicitly assume that a call to a function wants to call a function named
thus and see if some overload could fit the provieded tokens, this would
allow
void fun(enum { something, other } x);
to do the right thing as well.

/MF

Andrew Tomazos

unread,
Nov 29, 2018, 1:19:40 AM11/29/18
to std-pr...@isocpp.org
On Thu, Nov 29, 2018 at 4:05 PM Magnus Fromreide <ma...@lysator.liu.se> wrote:
On Thu, Nov 29, 2018 at 02:57:55PM +1000, Andrew Tomazos wrote:
> It seems to me that a function that takes a struct combined with C++20
> designated initializers gets us very close to named parameters:
>
> struct FooParameters {
>     int bar;
>     float baz;
> };
> float foo(FooParameters params) {
>    return (params.bar + 3) * params.baz;
> }
> int main() {
>    foo({bar: 4, baz: 3.2});
> }
>
> If we added some syntactic sugar that...
> 1. Removed the need to give the parameter struct type a name
> 2. Introduced the parameter struct members into the function definition
> scope.

I think this misses a lot of benefits

To which benefits do you refer?
 
and would propose two other rules:

1. Allow anonymous structs
   Similar to anonymous unions.
   This is already done by VC++ and as an extension by g++ and clang++.

$ cat > t.cc 
void f(struct {}) {}
$ g++ t.cc
error: types may not be defined in parameter types
 void f(struct {}) {}

Does this work in VC++?  Whats the gcc extension called?
 
2. Implicitly assume that a call to a function wants to call a function named
   thus

I don't quite follow sorry.  My proposal doesn't make any changes to name lookup of a function call or overload resolution, what changes to overload resolution are you proposing?
 
and see if some overload could fit the provieded tokens, this would
   allow
   void fun(enum { something, other } x);
   to do the right thing as well.
 
And what is the right thing?  I'm not sure I see how this is related?

Hyman Rosen

unread,
Nov 29, 2018, 10:50:28 AM11/29/18
to std-pr...@isocpp.org
On Wed, Nov 28, 2018 at 11:58 PM Andrew Tomazos <andrew...@gmail.com> wrote:
It seems to me that a function that takes a struct combined with C++20
designated initializers gets us very close to named parameters
This also seems easy to implement.
Thoughts?  Worth pursuing?

I think named argument association should be sui generis, not implemented
by weird class equivalents.  Doing the latter hearkens back to the days of OO,
where everything was defined in terms of virtual functions.  We should define
the behavior we want, not try for an equivalence that may or may not have dark
corners that don't quite match.

Ville Voutilainen

unread,
Nov 29, 2018, 10:57:21 AM11/29/18
to std-pr...@isocpp.org
I don't see how an automatic struct wrapper is necessarily a good
solution to the problem, since it changes the arity
of the function. This has practical consequences; the function is no
longer callable with separate unnamed
arguments, which is a horrible loss of functionality.

Bengt Gustafsson

unread,
Nov 29, 2018, 4:29:15 PM11/29/18
to ISO C++ Standard - Future Proposals
And it also misses the possibility to call different function overloads depending on the names provided at the call site.

Magnus Fromreide

unread,
Nov 29, 2018, 9:03:43 PM11/29/18
to std-pr...@isocpp.org
The parts are independent, the first part is about allowing anonymous structs
in places where structs are allowed today.

>
> > 2. Implicitly assume that a call to a function wants to call a function
> > named
> > thus
>
>
> I don't quite follow sorry. My proposal doesn't make any changes to name
> lookup of a function call or overload resolution, what changes to overload
> resolution are you proposing?

Consider

void f(struct { int a = 0; int b; });
void f(struct { int b; int c = 0; });

Which one should each of the following call?

f({a: 1, b: 2 }):
f({b: 1, c: 2 });
f({b: 1});

>
> > and see if some overload could fit the provieded tokens, this would
> > allow
> > void fun(enum { something, other } x);
> > to do the right thing as well.
>
> And what is the right thing? I'm not sure I see how this is related?

Look up types in the argument lsts of the possible functions.

I will readily admit that I have failed to convey the idea here.

/MF

Jake Arkinstall

unread,
Nov 29, 2018, 9:38:18 PM11/29/18
to std-pr...@isocpp.org
In the case of ambiguities, the compiler can error as it currently does with ambiguous function overloads. But given the work that would go into making that happen, a deeper language change without the indirection through structs might be better.

One thing I like about the struct idea is that it gives the control over named parameters to the API author, not to the user. Another benefit I see is that we don't need a change to function mangling, whereas function calls with parameters of the same types but different names would require such a change.

So I'm (tentatively) for it. But at the same time, I know that mine isn't a popular position.

We really need a poll to find out which named parameter features (and/or language changes) that people want, which features they don't want, and work from there. Otherwise what we end up with is many proposals and a different subset of people for and against each. As an example, I will defend the notion of authors having control over whether or not their functions can be called with named parameters until I see evidence that a strong majority would prefer the user to have that control - at which point the position becomes indefensible from a standards perspective.

Hyman Rosen

unread,
Nov 30, 2018, 11:06:14 AM11/30/18
to std-pr...@isocpp.org
On Thu, Nov 29, 2018 at 9:38 PM Jake Arkinstall <jake.ar...@gmail.com> wrote:
One thing I like about the struct idea is that it gives the control over named parameters to the API author, not to the user.

I think that's a bad idea.  The API author should be supplying good parameter names just like they supply good function names, good class names, good
enumeration literals, and so on.  Just because it does not matter now (and that's not really true, because functions should be documented and the documentation should be describing the parameters, so they should already have sensible names) is no reason to allow it to continue not to matter.
 
Another benefit I see is that we don't need a change to function mangling, whereas function calls with parameters of the same types but different names would require such a change.

No.  Named arguments (in my preferred and correct Ada style) are handled by the compiler without any ABI or mangling changes needed.  You cannot overload based only on different parameter names.  Just as now, multiple function declarations with the same function name and parameter types will declare the same function, even if the different declarations use different parameter names.

Overload resolution at the call site can use named argument association to help pick a function to call, but parameter names do not generate different functions.

Jake Arkinstall

unread,
Nov 30, 2018, 11:11:34 AM11/30/18
to std-pr...@isocpp.org
Both of these points are nonetheless opinions. The notion of allowing overloads of the same types with different parameter names is very much on the table - again, we have no poll results and no consensus, so the only thing we ever manage to obtain on these discussions is knowledge that everyone wants something different.

Hyman Rosen

unread,
Nov 30, 2018, 1:24:20 PM11/30/18
to std-pr...@isocpp.org
On Fri, Nov 30, 2018 at 11:11 AM Jake Arkinstall <jake.ar...@gmail.com> wrote:
Both of these points are nonetheless opinions. The notion of allowing overloads of the same types with different parameter names is very much on the table - again, we have no poll results and no consensus, so the only thing we ever manage to obtain on these discussions is knowledge that everyone wants something different.

And as exemplified by order of evaluation, we also have a history of coming to bad decisions, some based on specious arguments.
I'm not convinced that language design by plebiscite produces the best result (or even a good one).

Matthew Woehlke

unread,
Nov 30, 2018, 1:37:04 PM11/30/18
to std-pr...@isocpp.org
On 29/11/2018 21.03, Magnus Fromreide wrote:
> Consider
>
> void f(struct { int a = 0; int b; });
> void f(struct { int b; int c = 0; });
>
> Which one should each of the following call?
>
> f({a: 1, b: 2 }):
> f({b: 1, c: 2 });
> f({b: 1});

Somewhat OT, but... all of those will fail to compile because they are
not valid syntax.

Why aren't we using correct syntax in this discussion?

f({.a=1, .b=2}):
f({.b=1, .c=2});
f({.b=1});

--
Matthew

Matthew Woehlke

unread,
Nov 30, 2018, 1:48:56 PM11/30/18
to std-pr...@isocpp.org, Jake Arkinstall
On 29/11/2018 21.38, Jake Arkinstall wrote:
> In the case of ambiguities, the compiler can error as it currently does
> with ambiguous function overloads. But given the work that would go into
> making that happen, a deeper language change without the indirection
> through structs might be better.
>
> One thing I like about the struct idea is that it gives the control over
> named parameters to the API author, not to the user.

P1229 provides that.

> Another benefit I see is that we don't need a change to function
> mangling, whereas function calls with parameters of the same types
> but different names would require such a change.
I have an idea for 'function aliases' that would allow creating a
not-quite-overload¹ taking P1229 "strongly" named arguments that really
calls something else. (Usually, the "something else" would be the
ABI-existing version of the function that does not take "strongly" named
arguments.) The down side, however, is users can add such aliases, which
defeats your previous point. (OTOH, users can overload non-member
functions today, so you've already lost that battle. Possibly we could
limit method aliases to appearing in the class definition, which would
mitigate this to at least no worse than it is already.)

Basically, this gives you most of what everyone wants: opt-in, and the
ability to choose between name-based overloading and no ABI change.

(¹ The not-quite-overload mostly acts like an overload, except it is not
ambiguous with the signature it aliases.)

> We really need a poll to find out which named parameter features (and/or
> language changes) that people want, which features they don't want, and
> work from there. Otherwise what we end up with is many proposals and a
> different subset of people for and against each.

I actually started writing a paper to this effect, although it remains
to be seen if I actually finish it :-). (Basically, rather than being a
full proposal, the idea is to explain various folks' positions and
suggest how we can compromise... although the latter does end up being a
semi-proposal.)

> As an example, I will defend the notion of authors having control
> over whether or not their functions can be called with named
> parameters until I see evidence that a strong majority would prefer
> the user to have that control - at which point
> the position becomes indefensible from a standards perspective.

I'm on the fence here. In an ideal world, I agree, but I worry about
users wanting to use this feature with libraries that are unmaintained
or otherwise hard to change. That said, I don't think we can stop users
from adding overloads of free functions anyway...

--
Matthew

Matthew Woehlke

unread,
Nov 30, 2018, 2:01:39 PM11/30/18
to Hyman Rosen, std-pr...@isocpp.org
On 30/11/2018 11.05, Hyman Rosen wrote:
> Named arguments (in my preferred and correct Ada style) are handled by
> the compiler without any ABI or mangling changes needed. You cannot
> overload based only on different parameter names.

Let's say you have this feature implemented in such manner. How do you
make this (not) work:

int foo(int: a, int: b);
std::invoke(foo, .a=5, .c=12); // should be an error

--
Matthew

Hyman Rosen

unread,
Nov 30, 2018, 2:10:31 PM11/30/18
to std-pr...@isocpp.org

I'm not sure what you're getting at here.  In my version of named argument association,
the std::invoke function would need to have at least three parameters, two of which are
named a and b.  The std::invoke call is not a function call of foo, so the parameter names
of foo are irrelevant.

Matthew Woehlke

unread,
Nov 30, 2018, 2:56:54 PM11/30/18
to Hyman Rosen, std-pr...@isocpp.org
On 30/11/2018 14.10, Hyman Rosen wrote:
> On Fri, Nov 30, 2018 at 2:01 PM Matthew Woehlke wrote:
>> On 30/11/2018 11.05, Hyman Rosen wrote:
>>> Named arguments (in my preferred and correct Ada style) are handled by
>>> the compiler without any ABI or mangling changes needed. You cannot
>>> overload based only on different parameter names.
>>
>> Let's say you have this feature implemented in such manner. How do you
>> make this (not) work:
>>
>> int foo(int: a, int: b);
>> std::invoke(foo, .a=5, .c=12); // should be an error
>
> In my version of named argument association, the std::invoke function
> would need to have at least three parameters, two of which are named
> a and b. The std::invoke call is not a function call of foo, so the
> parameter names of foo are irrelevant.
Riiiight... So, what you're saying, is forwarding named arguments
doesn't work. That's a shame. It's also a feature that some people
(myself included) want.

Forwarding named arguments Just Works when names are part of the type
(e.g. P1229).

> I'm not sure what you're getting at here.

Really? std::invoke (and I mean *std::invoke*; I didn't chose that by
accident) transparently forwards its arguments to some other function,
also given in the call site. On its own, it takes whatever arguments you
give it, and just passes them along.

Therefore, it seems "obvious" that I should a) be able to pass named
arguments to std::invoke, and b) that std::invoke should call the
function passed to it using those named arguments. That is:

int foo(int: a, int: b);
std::invoke(foo, .a=5, .c=12);

...should result in a compile error, because I am trying to call `foo`
with non-matching arguments. More particularly, the above should have
the exact same effect as:

foo(.a=5, .c=12);

...which I believe pretty much all of us agree should be a compile error.

I should be able to do something similar with std::apply. More
generally, if I write something like:

auto t = std::make_tuple(12, .x=7);

...then `t` should continue to "know" that its second element isn't just
an `int`, but one with the 'name' "x".

--
Matthew

Hyman Rosen

unread,
Dec 2, 2018, 10:02:47 PM12/2/18
to Matthew Woehlke, std-pr...@isocpp.org
On Fri, Nov 30, 2018 at 3:44 PM Matthew Woehlke <mwoehlk...@gmail.com> wrote:
On 30/11/2018 15.31, Hyman Rosen wrote:

> On Fri, Nov 30, 2018 at 2:56 PM Matthew Woehlke wrote:
>> Riiiight... So, what you're saying, is forwarding named arguments
>> doesn't work. That's a shame. It's also a feature that some people
>> (myself included) want.
>
> I don't think that's a good thing to want, but I guess that's just me.
>
> I also don't want "pointer to function returning int with double parameter
> named 'a' and string parameter named 's'" as part of the type system.

I think I could go either way on that. For instance, if I have:

  template <typename Func>
  void foo(Func f)
  {
    f(.a=5);
  }

...don't I want this to be an error?:

  void bar(int: q);
  foo(&bar);

OTOH, pointers to functions with named arguments should probably
implicitly convert to pointers to functions with the same signature sans
name.

> How is your std::invoke call supposed to work, anyway?  Even if argument
> names were part of the function type (which they should not be), how is a
> named argument association supposed to pass through the std::invoke call?

...because a named argument has a different type than an unnamed
argument. See P1229. IOW, the argument type to invoke isn't `int`, it's
something like `std::arg<"x", int>`.

> Are you suggesting that a named argument association is a special object
> that carries the name with it, and can be passed to a function that doesn't
> have such a named parameter but can be passed along to something else that
> does?

Yes, exactly.

> I think we're once again in the land of "hey, this should do that!" and I
> think that's a bad way to design language features.

I don't think I can agree. If I expect a feature to work in a certain
way, and a proposed design doesn't give me that, doesn't that indicate
that maybe the design has a problem?

In my mind, it would be surprising if invoke/apply either didn't accept
named arguments at all, or effectively circumvented the protection (and
maybe other features?) that named arguments are supposed to provide.

I'd like to propose the opposite question... besides the above comment
about pointer types, and if this was implemented in a way that made it
*possible* to have named arguments in functions whose ABI does not
include the name, what objections do you have to argument names being
visible at the type system level?

--
Matthew

I think that having named argument association objects be things that aren't just
the same as the arguments themselves, and allowing named arguments to flow
through function calls into other function calls is just a recipe for disaster.  It strikes
me as causing similar issues to operator dot, namely, when does a name apply to
the thing itself and when does it apply to something else instead?  Furthermore,
initializer lists were implemented in the way you suggest, with strange interpolated
class objects, and that has not led to great satisfaction.

The beautiful thing about doing named argument association the Ada way is that
it's kept *out* of the type system.  It lets you reorder arguments on invocation, lets
you specify only those defaulted parameters that you want, and lets you document
your function calls at the call site.  You get overload resolution just to the extent
needed by the rest of these features. And you don't have to change any ABIs.

I promise you that making it part of the type system will end in tears, just like so
many other features of C++.

Matthew Woehlke

unread,
Dec 4, 2018, 10:53:52 AM12/4/18
to std-pr...@isocpp.org, Hyman Rosen
On 02/12/2018 22.02, Hyman Rosen wrote:
> The beautiful thing about doing named argument association the Ada
> way is that it's kept *out* of the type system. It lets you reorder
> arguments on invocation, lets you specify only those defaulted
> parameters that you want, and lets you document your function calls
> at the call site.
Having names be part of the type does not preclude any of this.

I'm on the fence about reordering, though...

One of my "eventual" objectives for named arguments is to replace
designated initializers with an "as if it existed" ctor rule. I believe
this can be done with no noticeable change in existing code. However, if
we do that, and then add reordering, we are reopening a can of worms as
far as designated initializers.

My preference is to get in a feature *without* reordering, first, and
then people that want reordering can try to add it later.

> And you don't have to change any ABIs.

I have a solution to that, that also allows you to add named arguments
to functions that don't already have them. You might not be able to
overload on name, though (I haven't fully thought it through, but I
think if you try to add an overload that would require named
overloading, you'll necessarily wind up with an ambiguous overload).

> I promise you that making it part of the type system will end in tears,
> just like so many other features of C++.

You keep saying that, but I haven't seen any *concrete* reason. Just a
lot of unspecific muttering.

--
Matthew

Hyman Rosen

unread,
Dec 4, 2018, 1:16:43 PM12/4/18
to std-pr...@isocpp.org
On Tue, Dec 4, 2018 at 10:53 AM Matthew Woehlke <mwoehlk...@gmail.com> wrote:
You keep saying that, but I haven't seen any *concrete* reason. Just a lot of unspecific muttering.

Here are some specific mutters:

int &a();
int &b();
a() <<= b();
a() <<  b();
a() <   b();

std::vector<int>         vi{3};
std::vector<std::string> vs{3};

std::vector<int>         zi{0};
std::vector<std::string> zs{0};

unsigned short u1 = 60001u;
unsigned short u2 = 59999u;
unsigned long u = u1 * u2;

For the sake of exposition, the library clauses sometimes annotate constructors with EXPLICIT. Such a
constructor is conditionally declared as either explicit or non-explicit (15.3.1). [ Note: This is typically
implemented by declaring two such constructors, of which at most one participates in overload resolution.
—end note ]

[ptr.launder]

I could come up with more.  It's a history of bad decisions, bad designs, and bad object models.
Part of it is excessive cleverness and part of it is inchoate "make this mean that" design, both of
which I find in your preferred version of named argument association.
Reply all
Reply to author
Forward
0 new messages