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:

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

export module lib;
export {int .a, int .b} foo() {
    return {.a = 5, .b = 3};

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@lists.isocpp.org> 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

On Mon, Jul 29, 2019, at 10:17 AM, Maciej Cencora via Std-Proposals wrote:

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});
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")
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.).


Std-Proposals mailing list

Std-Proposals mailing list