C++ Logo

STD-PROPOSALS

Advanced search

Subject: Re: [std-proposals] Named params via anonymous struct and function aliases
From: Omer Rosler (omer.rosler_at_[hidden])
Date: 2019-07-29 17:31:24


I have being working on this from a completely different angle, and will
publish this to the list soon.

I want to propose *named argument packs*

And the motivation is not only from named arguments perspective, but more
from *Multiple Return Values functions*

With structured binding in C++17, and P0889 for more copy elision (and my
proposal in this mailing list to make these mandatory), the style of
Multipl Return Function would become more popular.

There are a number of problems with it:
- Structured binding declaration allow you to forget the types you use, and
if you forget the names and order of parameters as well, you're in big
trouble:

```c++
struct result_t { int a; int b;};
extern result_t foo();
void bar() {
    auto&&[b, a] = foo(); //Oops, wrong order, compiles fine
    //...
}
```
- Problem with passimized NRVO in certain cases, which may be relied upon:
```c++
result_t foo(bool b1, bool b2, int& val) {
    int i = 5; // i can't be NRVOd even though the layout of the result
type is meaningless, only order of evaluation of the return expression
    if (b1) i = std::move(val); //this is to prevent reordering
optimization to actually allow it
    if (b2) return {.a = i, .b = 3};
    else return {.a = 3, .b = i};
}
```

The solution I found to be most tractable is the following two proposals:

1. Unpacking an aggregate type which is reachable but not visible (not part
of the public API of the module where it is defined) must be done with the
correct names and order of the type declaration.

2. Add a syntax to define anonymous non-copyable, non-movable structs with
given types, names and order *(but not memory layout)*
And make designated initializers evaluate to an unnamed struct of this type.

This magic struct would be convertible to any struct with the same types,
names, and order of parameters (which may affect overload resolution in
designated initializer lists, haven't really thought of this).

This can be zero overhead if UCE (P0889) is accepted, and will must be zero
overhead if my (to be written) proposal on mandatory NRVO is accepted

In total it looks like this:

```c++
//lib.cpp
export module lib;
export {int .a, int .b} foo() {
    return {.a = 5, .b = 3};
}

//user.cpp
module user;
import lib;
void bar() {
// auto&&[b, a] = foo(); //Compile Error

    auto&&[a, b] = foo(); //OK

    auto &&[c = .a, d = .b] = foo(); //OK, can rename to prevent name
collisions

    auto res = foo();
    auto b = res.a; //OK, I'm shooting my own leg
    auto a = res.b; //OK, I'm shooting my own leg
}
```
Now a function taking this sort of unnamed type declaration as a parameter
would have named arguments as this is the only way to create objects of
this type.

Note that calling this function from a different module with a
designated-initializer expression evaluates the expression to an object of
a different unnamed type and passes that type to the function call. Then a
conversion between the unnamed type would be triggered (they may have
different memory layouts).

How to prevent misuse:
1. Order Errors: Unpacking a non-exported type must be done with the
correct names and arguments.
2. Misused as structs: It is non-copyable and non-movable which means it is
usable in APIs only
3. DRY: It is unique within the scope it is defined. If it is defined in a
function declaration, it is unique to the namespace - i.e. if you have
named arguments that repeat in multiple places - you must pack them into a
struct.
4. Named arguments passing: Rule 1 also applies to aggregates, which means
the easiest way to pass arguments to an exported function taking
non-visible aggregate is by a designated-initializer expression.

Bikeshedding the syntax a bit:
I don't want the keyword `struct` to be part of the definition as it would
suggest memory layout assumptions. We don't want the memory layout to be
transparent as it is used for packing and unpacking a bunch of arguments
together.

What do you think of this approach?

On Tue, Jul 30, 2019 at 12:51 AM Henry Miller via Std-Proposals <
std-proposals_at_[hidden]> wrote:

> Syntax is not the hard part. There have been many syntaxes proposals with
> various pros and cons.
>
> The hard part is do we even want them. There are at least 4 different
> things you can do with named parameters and some are bad style that are bad
> enough to kill the entire thing. Figure out what all the things that can be
> done with named parameters, then decide what cannot be better done some
> other way (perhaps write the proposal for those), only then should we find
> a solution to the remainder.
>
> Good luck. If you push me I have some notes I could give you, but I lost
> interest when I realized how complex this seemingly simple and seemingly
> useful feature would be.
>
> --
> Henry Miller
> hank_at_[hidden]
>
>
>
> On Mon, Jul 29, 2019, at 10:17 AM, Maciej Cencora via Std-Proposals wrote:
>
> Hi,
>
> here is an idea on how to support named parameters in backward compatible
> way:
>
> allow single anonymous struct definition inside func parameter signature:
> void foo(struct { bool param1; int param2; })
> {
> impl(param1, param2);
> }
>
> The signature:
> void foo(struct { bool param1; int param2; })
> is equivalent to:
> void foo(bool param1, int param2).
>
> with the added benefit that it supports named parameters via designated
> initializers:
> foo({.param1 = false, .param2 = 10});
> or
> foo(false, 10);
>
> Designated initialization rules could be relaxed for this specific case to
> allow initializers be in different order than declared.
>
> This won't brake existing code, and we can introduce named params for
> existing API via function alias mechanism in backward compatible way:
>
> assuming we have POSIX fopen:
> int fopen(const char* pathname, const char* mode);
>
> we standardize new function alias:
> using fopen(struct { const char* pathname; const char* mode;}) =
> fopen(pathname, mode);
>
> and now user can do:
> fopen("/my/file", "w")
> or
> fopen({.pathname = "/my/file", .mode = "w"});
>
>
> Function alias mechanism could be re-used in many more scenarios (e.g.
> during refactoring, when changing func name, or re-ordering parameters,
> etc.).
>
>
> Thoughts?
>
> Regards,
> Maciej
> --
> Std-Proposals mailing list
> Std-Proposals_at_[hidden]
> http://lists.isocpp.org/mailman/listinfo.cgi/std-proposals
>
> --
> Std-Proposals mailing list
> Std-Proposals_at_[hidden]
> http://lists.isocpp.org/mailman/listinfo.cgi/std-proposals
>



STD-PROPOSALS list run by std-proposals-owner@lists.isocpp.org

Standard Proposals Archives on Google Groups