C++ Logo

std-proposals

Advanced search

Re: Named params via anonymous struct and function aliases

From: Henry Miller <hank_at_[hidden]>
Date: Thu, 15 Aug 2019 16:51:45 -0500
I realize it has been almost a month, I've been meaning to respond. However unlike most proposals for Named parameters you start with a real problem not a solution in search of a problem. This difference makes it much harder to give any response. Congratulations to you for that.

I'm not in favor of this now because I think we should explore strong types more first. That is I would prefer to not forget the type used in the first place. I think we should explore options to make us not forget the type.

Then we should explore options to make creating string types from other types easy. We have weak types, in typedef and alias, the problem is "using newInt = int;" is you can do "int i; newint b = i;", which in a strong type should not compile.

Once we have the above (and possibly a few other things I haven't thought of), there room to solve cases where strong types are not the answer. Thus I said I'm not in favor of this now - once we have found the end of other solutions named parameters is the only solution I can think of to some problems. That they also solve your problem (in at least some forms) is a bonus in favor of this form.


-- 
 Henry Miller
 hank_at_[hidden]
On Mon, Jul 29, 2019, at 5:31 PM, Omer Rosler wrote:
> 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

Received on 2019-08-15 16:53:47