Named aguments, take 9000

205 views
Skip to first unread message

mihailn...@gmail.com

unread,
Jul 30, 2018, 2:58:25 PM7/30/18
to ISO C++ Standard - Future Proposals
Here comes another post on named arguments ("keyword" arguments).

I am sure there is no need for an introduction to the topic, so I will go straight. 


Part 1. Inferior workarounds

1. "Just use types instead"

This does not work in cases where we have more then one argument of the same type.

Even a trivial example like setSize(int width, int height) works only because we use an implicit convention - width comes first.

Note, width and height are cross-assignable, which makes introducing a new type 100% superfluous. 
Also note the high cost of maintenance of additional classes.

If we change the example to child(name mother, name father), we completely lose expressiveness:

child("Tony Robinson", "Dustin Hana");

This solution also does not scale well with libraries which use predominately basic types like integers and floats - math and physics libraries are a great example of this.
Even the standard library is not an exception:

// note, angle phi and coordinate x are both the same type

sph_legendre( unsigned l, unsigned m, double θ );
assoc_legendre( unsigned int n, unsigned int m, double x );

// the call to such functions is, understandably, quite unclear

sph_legendre(3, 0, 1.2345);
assoc_legendre(2, 0, .5);


2. "Just use named functions"

Named functions are poor-man's alternative to named arguments - they can't name individual arguments and do not scale.

sph_legendre(3, 0, 1.2345); //< named, yet still unclear

child::withMotherAndFatherAgedWithSiblings(. . .); //< does not scale well to multiple arguments

Other than that, if the function is a static function, used as "named constructor", this means we lose all benefits of a constructor - library and metaprogramming construction support, deduction guides, explicitness of what creates instances, and more.


3.  Use "multi-stage construction" [if you need multiple arguments in a constructor]

Read more about this advice here

This is an ok advice as long as the design of the class is not changed to enable this kind of initialization, otherwise it is an antipattern - one both introduces new public interfaces and additional mutability into the class.

Even when it is ok, it is far from great - the constructor, along its benefits, is used only partially; it is not clear if the order function calls is important; you can't easily create a const variable; the solution ends up actually extremely verbose.



This solution does not scale and it has massive code duplications. It is also by no means obvious what is expected from the user of the interface and if order of calls is important. 


5. Use an arguments aggregate class, together with designated initializers.

Multitude of issues - maintenance cost, code duplication, does not scale, problems with overloading, the interface actually hides the arguments into a new object, implementation is severely affected by the interface.


6. Lastly, almost all of the alternatives have an additional drawback - the naming is mandatory. In practice, we often have variables, which can stand in for the arguments names.

const auto mother = ". . .";
const auto father = ". . .";

child::withMotherAndFather(mother, father); //< needlessly verbose 



Part 2. Problematic solutions


1. "Just works"

This solution envisions C/C++ arguments be used as-they-are and the client opts-in to get a warning on mismatch. 

First, there is the fundamental problem with the fact that something, which has been private for 40+ years, is now essentially made public. This is wrong on its own. 
Even if we ignore that, the solution is not practical:
  • Different implementations have different naming (and we can't force standardization of all arguments, forever); 
  • Any change in the implementation naming will trigger false positives (and the majority of the library code has no separate implementation)
  • This solution is too taxing for library creators - suddenly we demand, stable arguments for all your interfaces, forever. And you have nothing to do about it.
  • No interface ever needs all arguments named.
  • Warnings are actually not good enough - the type of problems, rising from wrong arguments are just too severe, as they are, in a way, a breach of contract.

2. "It must [just] work with the standard library" 

This requirement is unfounded. 
Most if not all of the arguments made in  "Just works" apply here as well, but there is one additional observation - the standard library does not really need named arguments.
The reason for this is simply, because it was designed that way. There are multiple ways, used to get around current limitations, first and foremost - named functions, but also use of types (chrono) and small number of similar arguments (algorithms) 

This is not to say, the standard library can't take advantage of named arguments, but this can happen only in specific places and after careful consideration - the standard library will never need all arguments to be named! 


3. The developer [just] marks arguments to become names

This has the reverse problem such as, the public interfaces dictates the implementation - the library author is forced to use names, not helpful to him, but helpful to the client, calling the function.
This is not good. In a way, for the client to win, the author has to lose, yet it is the library author who actually uses these arguments! 


4. "Names must be mandatory/part of the signature"

This is a non-starter for C++ because of generic code and variadic forwarding.

Besides, tag arguments fit that role already, and we can improve on that.


Part 3. Suggested approach


1. Names are separate from the arguments. This leaves the implementation free to change the names it uses and also lets user-facing names be specifically designed to be a public interface.

child(name mother: mom, name father: dad); 

copy(const Path& src, const Path& to: dst); //< copy("/path/to/image", to: "/some/other/path")

replace(const QByteArray& ol, const QByteArray& with: nw) //< var.replace("yes", with: "no") 

replace(int at: pos, int count: len, const QByteArray& with: after) //< var.replace(at: 5, count: 4, with: "Bye")

template<class D, class K>
setData(D&& v, const K& forKey: key); //< setData("data"s, forKey: "one"s); 

If the argument is omitted, the name can be reused as an argument, but only if there is no default value.

slider(int value:, pair<int, int> range: minmax = {0, 99});


2. Names are not mandatory and don't change the signature

As mentioned, often times we have suitably named arguments, no need for extra verbosity, be it as a function or argument name:

copy(path, destpath); //< clear enough

Pointers and function objects are not names-aware:

using p = void(const Path&, const Path&);
p = &copy;

p("/path/to/image", "/some/other/path");

//p("/path/to/image", to: "/some/other/path"); //< error

p = &rename; //< allowed


3. Names are declared using the same rules as default arguments - only one visible declaration must have names.

namespace {

auto child(name mother: mom, name dad); 
// auto child(name mother: mom, name dad); //< error
// auto child(name mom: mom, name dad); //< error
auto child(name mutter, vater); 
auto child(name mother, name father)
{
  . . . 
}

}//< ns


4. The argument name is a compiler-brokered "handshake" b/w the library author and the user and both must agree on it. 

It is an error to pass a name to an argument that it is not declared to have name.

void func(int arg);

// user

func(arg: 5); //< error

There is one exception. If, and only if, the argument is forwarding reference and it is forwarded,
then the forwarding function is allowed to be called with an argument name, even if one is not declared for that argument.


5. Because names are purely compile time, and perfect forwarding is always inline, names are able to change call sites.

This is done by the compiler alone, by prepending calls to std::forward with the name, used on the forwarded argument, for the call of the enclosing function.

void func(int& foo: arg);

template<class T>
void fwdfunc(T&& arg) //< no name for the argument
{  
  func(std::forward<T>(arg));
}

// user

int val = 5;
fwdfunc(foo: val) //< allowed

// generates

void fwdfunc(int& && arg) 
{  
  func(std::forward<int&>(arg)); 
// =>  func(foo: std::forward<int&>(arg))
// =>  func(foo: int&)
}

If the forwarding function declares a name for that argument, the user specified name is checked against that name instead and std::forward is called normally.


6. Calling a function with a name for an argument, but with no value for that argument, implies default value.

This rule has two important applications.

First it allows us to explicitly pass defaulted arguments.

func(int a, int b: b = 10. int c: c = -1, string text = "hello");

func(10, b:, c:, "bye");

Second, it enables using default arguments in environments where default arguments are currently considered bad practice.

func(Somethings some, M magic: m = "goodMagic");

func(something, magic:); //< clear, magic is involved


7. Names do not contribute to overload resolution.

Argument names are not part of the type system and not part of the function name, they are last minute confirmation, a handshake, after all the details are resolved. 

This way we don't complicate overload resolution further and prevent wrongly remembered, and/or wrongly typed, names to pick the incorrect overload or to fail finding an overload at all.
Letting names "guide" or "early out" overloading means making them stronger or equal to types which is unsound, considering they are optional and not part of the name/signature or the function.

In other words, the times names will help will eventually be countered by the times names will get in the way, yet in the former case, changes to the overloading are needed. That's a bad trade off.


Part 4. What about the standard library?


As said, the standard library as a whole does not really need named arguments, yet some improvements could be made.
Some interfaces could be streamlined and some - made more clear.  

For example the _n versions for most algorithms introduce this new function name not because the function does something different, but only to make the calls more clear.
This task could ideally be accomplished by a single named argument:

void fill( OutputIt first, Size count: count, const T& value );

With this interface the user is free to name the, probably confusing, argument

fill(begin, 5, -1); //< confusing (but correct - all fill()-s are under the same name)
fill(begin, count: 5, -1); //< clear
fill(begin, count, -1); //< also clear, without unneeded over-specifications

Even more interesting is the case with search_n. Few things make this interesting. 
First is the fact, that although it ends with _n, yet this indicates something completely different then with all other algorithms.
The suffix does not indicate the count of items to operate on, but the count of consecutive values, equal to the 'value' argument. 

Second, the suffix does not help making the call more clear, not to the slightest, but what is more interesting - making the arguments names public, does not make things any more clear either!
What we need instead is, actually to do a design work on a argument name, or couple of argument names, in order to make the interface clear and understandable. 

Here are few suggestions to highlight the possibilities 

FrwIt search( FrwIt first, FrwIt last, Size encounters: count, const T& ofValue: value ); 
FrwIt search( FrwIt first, FrwIt last, Size consecutiveItems: count, const T& equalTo: value ); 
FrwIt search( FrwIt first, FrwIt last, const T& forValue: value, Size repeated: count );

// use

search(begin, end, encounters: 5, ofValue: 0);
search(begin, end, consecutiveItems: 5, equalTo: 0);
search(begin, end, forValue: 0, repeated: 5);

This shows clearly, named arguments have no alternative when it comes to expressiveness. It also shows the importance of them being optional, as well as being separate from the argument.

Let's see some more examples where using named arguments make the code notably more clear

create_directory("path/to/directory", attributesFrom: "path/to/file");

nth_element(begin, partitionPoint: (end - begin)*.33, end);
replace(begin, end, 5, with: 1);
sample(begin, end, begin1, samples: 1000, gen);

copysign(2, from: -1 ); 
assoc_laguerre(n: 1, m: 10, x: 0.5);
sph_legendre(n: 3, m: 0, theta: 1.2345);
ellint_1(k: 0, phi: acos(-1)/2);

It bears repeating, names are optional - the user is free to use them only where he considers, there is a need for them.


Part 5. What makes this different the other named arguments proposals?


1. Argue, names need to different from the argument they represent, most of the time, not by exception.


2. Argue, no API needs all arguments named. Names must have targeted, carefully designed use. This includes the standard library.

For example, there is an overarching convention in the standard library, that algorithms take an input range as the first two arguments. These don't need to be named. Some algorithms, which write the result to a new location, take an output iterator as a third argument. This argument should be named, but it is not essential. And then, there is the regex library, which takes output as the first argument. This argument really should be named as it breaks any previous conventions.

regex_replace(out: begin1, begin, end, vowel, "*");

This way the code is both self-documenting and more resilient to calling the incorrect overload.


3. Argue, "forwarding" of names is doable without involving the type system.


4. Argue, not contributing to the overload resolution is a good thing. 


Conclusion

Contrary to the common perception, named arguments are actually an enabling technology - they enable interfaces, impractical or even dangerous otherwise. 
When used for constructors they promote immutability and minimalistic public interface. For regular functions, they promote natural function naming - what the function does - and natural number of arguments, not limited by readability. They lower maintenance cost and code size by eliminating superficial constructs, used to work around lack of named arguments.


Thank You for Your time!

Nicol Bolas

unread,
Jul 30, 2018, 4:28:34 PM7/30/18
to ISO C++ Standard - Future Proposals, mihailn...@gmail.com
So, you're basically proposing P0671: Self-explanatory Function Arguments. The core principle of both is that it has no behavioral impact. You can't overload off of it, you can't change the meaning of the code. It's just notation to help the compiler give warnings, and for programmers to clarify what's going on in complex situations.

There are differences. P0671 takes the parameter name as the name of the argument, while yours creates a new syntax to declare such names. That deals with one of the standard library problems, in that implementations aren't required to give function parameters any particular name. Many implementations will use `_Capital` names for such parameters, to avoid conflicts with user-defined macros and such.

Of course, your way still breaks with user-defined macros too, but there's nothing to be done about that. The real thing however is that modules ought to be able to get around this. In a modular world, we can declare that the standard library imported through modules must use the names specified in the standard.

If that is done, then the principle reason for declaring parameters with names goes away. There are still other reasons, such as having the freedom to change them later. But since P0671 makes a mismatch merely a diagnostic rather than ill-formed code, there isn't such a need for it.

Also, your proposal allows defaulting parameters in the middle of a parameter sequence. I think this goes against the feature being pure notation, as it allows you to do something you otherwise couldn't do. Not to mention, it offers an incentive to give parameters names regardless of whether it's genuinely useful to do so, just so that people can not pass such parameters.

I consider your item #3.5 to be pure QoI. If an implementation wants to peer through a template instantiation to discover where a parameter is being forwarded to, it can, and it can issue a warning based on what it finds. But there should be no requirement for it.

Jake Arkinstall

unread,
Jul 30, 2018, 8:21:52 PM7/30/18
to std-pr...@isocpp.org
Being pedantic, I contest your very first point, on the grounds that better typing would make all of your examples a compilation error if the wrong call is made. Phantom types are often used to combat this exact problem, bringing a lot of beautiful algebraic type structure along for free.

In this sense, an angle and a distance certainly aren't equivalent (regardless of their underlying representation), and a height and width aren't the same thing but can be converted explicitly. For the child, a mother and father can be cast to distinct types at the call site, and an incorrect order would be a compile error.

This isn't to say that I don't see some purpose in named arguments, but I just can't think of any example where the arguments are truly semantically equivalent and the ordering isn't self-evident (as is the case with symmetric and anti-symmetric functions, the latter by convention).

I'm sure there are probably some cases where named arguments solve a problem that aren't already solved via sensible types, but some more creative examples might be in order.

floria...@gmail.com

unread,
Jul 31, 2018, 5:09:50 AM7/31/18
to ISO C++ Standard - Future Proposals
I have the impression your new proposals are more and more complicated without any real benefit.
In fact, I think your very first proposal on named argument was both simpler and better.

Even though, I would say it was still a bit complex and could be simplified further to solve some of the issues you had back then.

mihailn...@gmail.com

unread,
Aug 1, 2018, 6:02:28 PM8/1/18
to ISO C++ Standard - Future Proposals, floria...@gmail.com
Nicol, the most important difference is the fact, names are different from arguments - this radically improves the expressiveness of names (names like 'equalTo', 'with', 'including', 'forValue'  become feasible) and frees the library developer to use and change the arguments at any time.

Also, wrong names are errors, not a warnings. 
About forwarding, we formerly must handle the case as the implementation must know against which call to check the name. I see forwarding as pretty much a must, considering we want to make_objects and constructors are number one client of named arguments.


Jake, typing does not scale and come at maintenance and code size cost (why is the stl not using different types for x and theta?). 

More importantly, at some point special types no longer make semantic sense - width and height are not different because it is perfectly normal to size.setWidth(size.height()). 
Mother and Father are different, but MotherName does not make sense. If we invent types that are useless beyond a function call - we have a problem with the function call itself. 
 
But there is more, how do you make typed arguments optionally typed? Do you really want to affect overloading, function points, etc. just to get naming?
And how to you name types for count, n, k, theta, phi, for, excluding, partitionPoint etc, etc. 

Of course, types come first, but sometimes even different types
need to be annotated - setData(data, forKey: myObject); nextEvent(Mask, until: Date)

Most often however this comes up in non-public interfaces, the ones that are not as pretty and minimalistic. 

It is not unheard of to have a class to take few different types upon creation, and these classes to have no self-evident relation to the created class .
And because this is not a public interface one will never going to make pretty wrapper-classes for the call site - one almost certainly will use comments, or worse. 
And not just class creation, functions as well - often different types does not make things any more less confusing and we are forced to have long, explanatory names to the function itself and/or comments. 


florian, not sure which one you have in mind. If it was the one with tag arguments, it is not an alternative at all - both complement each other. 
And, honestly, I don't think this one is complex - 
  • arguments can have a second name, the user can pass that second name.
  • it is an error to have name mismatch.
  • names are declared by the same rules as default value.
  • the compiler passes the name on forward(), if not already "caught" by the enclosing call.
  • (optional) the user can omit the value, if a name is specified and there is a default value.
  • (tiny optional) if no argument is specified, only name, the name becomes the argument.
That is literally it.   

Nicol Bolas

unread,
Aug 1, 2018, 8:46:32 PM8/1/18
to ISO C++ Standard - Future Proposals, floria...@gmail.com, mihailn...@gmail.com
On Wednesday, August 1, 2018 at 6:02:28 PM UTC-4, mihailn...@gmail.com wrote:
Nicol, the most important difference is the fact, names are different from arguments - this radically improves the expressiveness of names (names like 'equalTo', 'with', 'including', 'forValue'  become feasible) and frees the library developer to use and change the arguments at any time.

Also, wrong names are errors, not a warnings. 
About forwarding, we formerly must handle the case as the implementation must know against which call to check the name. I see forwarding as pretty much a must, considering we want to make_objects and constructors are number one client of named arguments.

And that's exactly why it should be a warning, not an error. Because you're not going to always be able to tell.

Let's say you have your own `make_` function, but it logs the arguments with something like `(log(std::forward<Args>(args)), ...)` before calling the constructor. That means you're going to forward each parameter to a function. That function will be overloaded on each supported type to extract the value for the logging operation (perhaps with some template default). So... how does that work?

If you named an argument, and you have this rule that can look into the function and see if it's calling the right function, what happens? It sees that `log` doesn't take a named argument with that name, so it fails.

That's not good; you'd be breaking debugging information.

In fact, I just realized that `std::forward` itself wouldn't work, since its argument does not have the corresponding name. So how can you call it with a named parameter?

And if you special-case this as some magical property of `std::forward`... what happens if people use the equivalent `static_cast`? Or is that special-cased as well?

If this is going to work, you need to lay down exactly what the rules are for this forwarding of named parameters.

Ville Voutilainen

unread,
Aug 1, 2018, 8:53:13 PM8/1/18
to ISO C++ Standard - Future Proposals, floria...@gmail.com, mihailn...@gmail.com
Furthermore: does an ill-formed named-arg call SFINAE? If it does,
it's going to be a *really* hard sell
that to determine whether the call is or is not well formed, function
bodies should be inspected.

Florian Lemaitre

unread,
Aug 2, 2018, 6:49:18 AM8/2/18
to Михаил Найденов, ISO C++ Standard - Future Proposals
2018-08-02 0:02 GMT+02:00 <mihailn...@gmail.com>:
florian, not sure which one you have in mind. If it was the one with tag arguments, it is not an alternative at all - both complement each other. 
And, honestly, I don't think this one is complex - 
  • arguments can have a second name, the user can pass that second name.
  • it is an error to have name mismatch.
  • names are declared by the same rules as default value.
  • the compiler passes the name on forward(), if not already "caught" by the enclosing call.
  • (optional) the user can omit the value, if a name is specified and there is a default value.
  • (tiny optional) if no argument is specified, only name, the name becomes the argument.
That is literally it.   
 

So let me tell you what I was thinking:

There exist a common, yet unutterable, where tag types live (lets call it tag_ns to simplify).

When you declare a function with tags:
int foo(tag_name: float f);
The compiler creates a tag type in this namespace called tag_name, and transform the declaration like that:
int foo(tag_ns::tag_name, float f);

When you call a function with a tag:
foo(tag_name: 1.f);
The compiler creates a tag type in this namespace called tag_name, replaces the expression with a call like that (without even needing to know the declaration of the function):
foo(tag_ns::tag_name{}, 1.f);

If two functions (even in different namespaces) use the same tag name, they will have the same tag type, which is fine.
The overload rules and forwarding rules are not affected as they are applied after the aforementioned transformations.
ADL is not modified as the tag does not depend on the function namespace.
If a tag type already exist in the same TU, then it used (instead of recreating another one).
If the tag is created in multiple TUs, the same rule applies as for regular types: definitions must match (this is the compiler job).

If you mistype a tag, you get a hard error as there is no overload for it (unless you have some catchall parameters...).

Tags wouldn't need any special treatment (apart from parsing).
This would just be syntactic sugar over well known behaviors: it would just work.

The main feature here would be the automatic introduction of types. A bit like a template instantiation. It is implicit.

In your first proposal, you mentioned you wanted to be able to do something like that:
any(in_place<string>: "");
I don't think it is useful. you are not naming an argument here.

Now, to be fair, you could do something equivalent in C++20:
int foo(std::tag<"tag_name">, float f);

foo
("tag_name"tag, 1.f);

But the shorthand syntax would be very nice.
Moreover, such a shorthand syntax would give people incentives to use it (I consider it a good thing).
Let's make a quick poll. Which one do you prefer? (try to see it as a newcomer to the language)
std::find_if(v.begin(), v.end(), functor);
std
::find(v.begin(), v.end(), "if"tag, functor);
std
::find(v.begin(), v.end(), if: functor);

I personally prefer the third one (I would like it even more with ranges).
This is not really about the possibility to do it now, but how the syntax looks.

Overall I want to clarify: that's my opinion, and I'm pretty sure some will disagree, but I really think it is worth pursuing.
 

Henry Miller

unread,
Aug 2, 2018, 7:11:26 AM8/2/18
to std-pr...@isocpp.org
While those are good points, I would rather the rule be error. If that means named arguments are not compatible with std::forward or other language constructs so be it. I would prefer the compiler tell me that named arguments are illegal in some context than to have the name ignored.

The problem with warning is we imply there are cases of false positives. That destroys one of the reasons to name an argument. DoSomething(height: 10) better not act on a width. If I can't use the named argument in whatever context, at least I know to double check. If I can use it, but sometimes I get a warning I'll ignore the warning (if I wasn't going to ignore it I'd turn on warnings are errores) I've lost the value of being sure that my name ensured correctness. 

I might be okay with some syntax that indicates any name is acceptable for some argument. Might because I have a suspicion I can't put into words that ultimately it would be abused. In the context of log it makes sense, but in the context of std::forward we can't cheat this way.

Richard Hodges

unread,
Aug 2, 2018, 7:14:24 AM8/2/18
to std-pr...@isocpp.org
I'm stating to see value in this approach. Basically it's automatic tagging.

FWIW I agree, the third looks the most favourable.

What would look even more favourable IMHO would be:

std::find(v.begin(), v.end(), if=functor)

I appreciate that this would require a little more context-sensitive syntax parsing, but it's then as readable as python's named arguments.

Similarly, if we could declare defaults in the declaration in spite of c++'s current limitation of only allowing defaults on the trailing arguments, this would be great, no?

example:

auto bind_and_listen(socket: socket& sock, 
                     iface: socket_interface_def = inet46_interface(0), 
                     port: int port = 0, 
                     backlog: int backlog = 5);

possible usages:

socket s;
bind_and_listen(s, port:1025);  // listen on all interfaces on port 1025 with backlog 5
bind_and_listen(s, iface = ipv4(127,0,0,1), backlog=20);  // listen on localhost on auto-allocated port with backlog 20
bind_and_listen(s);  // listen on all interfaces on auto-allocated port with backlog 5



 
 

--
You received this message because you are subscribed to the Google Groups "ISO C++ Standard - Future Proposals" group.
To unsubscribe from this group and stop receiving emails from it, send an email to std-proposal...@isocpp.org.
To post to this group, send email to std-pr...@isocpp.org.
To view this discussion on the web visit https://groups.google.com/a/isocpp.org/d/msgid/std-proposals/CANaF6iiMz883O7FdLiKbQ1RxfkcMJ-RwXK%2Bi4JEvKMs616VF5A%40mail.gmail.com.

floria...@gmail.com

unread,
Aug 2, 2018, 7:39:04 AM8/2/18
to ISO C++ Standard - Future Proposals
Problem is: it would give ambiguous syntax:
int foo = 5;
bar
(foo=3);
// bar(std::tag<"foo">{}, 3);
// or
// bar( (foo = 3) );
// ?

That's why I think the colon is the right choice.
 

Similarly, if we could declare defaults in the declaration in spite of c++'s current limitation of only allowing defaults on the trailing arguments, this would be great, no?

example:

auto bind_and_listen(socket: socket& sock, 
                     iface: socket_interface_def = inet46_interface(0), 
                     port: int port = 0, 
                     backlog: int backlog = 5);

possible usages:

socket s;
bind_and_listen(s, port:1025);  // listen on all interfaces on port 1025 with backlog 5
bind_and_listen(s, iface = ipv4(127,0,0,1), backlog=20);  // listen on localhost on auto-allocated port with backlog 20
bind_and_listen(s);  // listen on all interfaces on auto-allocated port with backlog 5


For this, I would say you could do it in library:

template <class... Args>
struct PythonArgs : std::tuple<Args> {
 
PythonArgs(Args args) : std::tuple<Args>(std::forward<Args>(args)) {}

 
template <std::size_t I>
 
auto get(std::integral_constant<std::size_t, I> = {}) const; // positional parameters

 
template <class T>
 
auto get(T) const; // keyword parameters
};

template <class... Args>
auto bind_and_listen(Args&&... args) {
 
PythonArgs pargs(std::forward<Args>(args)...);
 
auto socket = pargs.get(socket:);
 
auto port = pargs.get(port:);
 
/* ... */
}

That would be much easier to standardize, while still being powerful enough for most cases.

Nicol Bolas

unread,
Aug 2, 2018, 9:43:29 AM8/2/18
to ISO C++ Standard - Future Proposals, mihailn...@gmail.com, floria...@gmail.com
On Thursday, August 2, 2018 at 6:49:18 AM UTC-4, Florian Lemaitre wrote:
Now, to be fair, you could do something equivalent in C++20:
int foo(std::tag<"tag_name">, float f);

foo
("tag_name"tag, 1.f);

But the shorthand syntax would be very nice.
Moreover, such a shorthand syntax would give people incentives to use it (I consider it a good thing).
Let's make a quick poll. Which one do you prefer? (try to see it as a newcomer to the language)
std::find_if(v.begin(), v.end(), functor);
std
::find(v.begin(), v.end(), "if"tag, functor);
std
::find(v.begin(), v.end(), if: functor);

I personally prefer the third one (I would like it even more with ranges).
This is not really about the possibility to do it now, but how the syntax looks.

I personally prefer being able to know how many arguments a function takes. With the `tag`, the number of arguments is clearly 4. With the colon syntax, it looks like 3, even though its 4.

Also, I personally prefer #1, since there's no ambiguity on what's going on. That is, even if you have tag generators like this, they shouldn't be used here.

mihailn...@gmail.com

unread,
Aug 2, 2018, 9:49:19 AM8/2/18
to ISO C++ Standard - Future Proposals, floria...@gmail.com, mihailn...@gmail.com


On Thursday, August 2, 2018 at 3:46:32 AM UTC+3, Nicol Bolas wrote:
On Wednesday, August 1, 2018 at 6:02:28 PM UTC-4, mihailn...@gmail.com wrote:
Nicol, the most important difference is the fact, names are different from arguments - this radically improves the expressiveness of names (names like 'equalTo', 'with', 'including', 'forValue'  become feasible) and frees the library developer to use and change the arguments at any time.

Also, wrong names are errors, not a warnings. 
About forwarding, we formerly must handle the case as the implementation must know against which call to check the name. I see forwarding as pretty much a must, considering we want to make_objects and constructors are number one client of named arguments.

And that's exactly why it should be a warning, not an error. Because you're not going to always be able to tell.

Let's say you have your own `make_` function, but it logs the arguments with something like `(log(std::forward<Args>(args)), ...)` before calling the constructor. That means you're going to forward each parameter to a function. That function will be overloaded on each supported type to extract the value for the logging operation (perhaps with some template default). So... how does that work?


There are few ways around this

template<class... T>
Object make(T&&... args)
{
    log(args...);

    return Object(std::forward<T>(args)...);
}


This works as per current rules.


However we could improve the rules to create a pass-trough

template<class T>
decltype(auto) log(T&& a) 
{
    std::count << a;

    return std::forward<T>(a); 
}


If we return a (possibly annotated) forward, and the return type is deduced (or we invent a attribute for that), then the call of the function is annotated instead.

Object(log(std::forward<T>(args))...);

// generates
// => Object(foo: log(foo: something));


...

In fact, I just realized that `std::forward` itself wouldn't work, since its argument does not have the corresponding name. So how can you call it with a named parameter?

And if you special-case this as some magical property of `std::forward`... what happens if people use the equivalent `static_cast`? Or is that special-cased as well?


We can declare reference collapsing actually forces the compiler to add annotation, then use the above new rule to create std::forward itself.



If this is going to work, you need to lay down exactly what the rules are for this forwarding of named parameters.

Absolutely. But it is very important both to be errors and to work with it. To work is important, because this is standard and recommended way of creating objects, not just some minor library helper.

To be an error is important because A) It is breach of contract in basically all cases to get the arguments wrong. B) Nowadays we have very aggressive static analyses which are great but tend to generate false positives - warnings might go unnoticed.  


About macros. I believe names will not break on user-defined macros, because the compiler should not look for macros when he sees something:

If the standard library decides to have names for a function, the implementation will just add another declaration with names.

#if __supports_names__
double assoc_legendre( unsigned n:, unsigned m:, double);
#endif

// old code stays as-is 100%

double assoc_legendre( unsigned __n, unsigned __m, double __x )
{
  // . . .
}



About SFINAE - names are outside the typesystem. They kick in after all types-related rules in order to catch semantic errors, not naturally mappable to types.  
If there is a name mismatch and one removes the name on the call site, the call succeeds because, to be caught that late, it means the types are correct as far as C++ is concerned.  
As a side effect overload resolution failure will be reported before name mismatch (if any), however the compiler will be able to use the names on both call and declaration to create way, way better diagnostics. 



Florian, I will answer later separately about tags

Nicol Bolas

unread,
Aug 2, 2018, 11:21:12 AM8/2/18
to ISO C++ Standard - Future Proposals, floria...@gmail.com, mihailn...@gmail.com
On Thursday, August 2, 2018 at 9:49:19 AM UTC-4, mihailn...@gmail.com wrote:
On Thursday, August 2, 2018 at 3:46:32 AM UTC+3, Nicol Bolas wrote:
On Wednesday, August 1, 2018 at 6:02:28 PM UTC-4, mihailn...@gmail.com wrote:
Nicol, the most important difference is the fact, names are different from arguments - this radically improves the expressiveness of names (names like 'equalTo', 'with', 'including', 'forValue'  become feasible) and frees the library developer to use and change the arguments at any time.

Also, wrong names are errors, not a warnings. 
About forwarding, we formerly must handle the case as the implementation must know against which call to check the name. I see forwarding as pretty much a must, considering we want to make_objects and constructors are number one client of named arguments.

And that's exactly why it should be a warning, not an error. Because you're not going to always be able to tell.

Let's say you have your own `make_` function, but it logs the arguments with something like `(log(std::forward<Args>(args)), ...)` before calling the constructor. That means you're going to forward each parameter to a function. That function will be overloaded on each supported type to extract the value for the logging operation (perhaps with some template default). So... how does that work?


There are few ways around this

template<class... T>
Object make(T&&... args)
{
    log(args...);

    return Object(std::forward<T>(args)...);
}


This works as per current rules.


However we could improve the rules to create a pass-trough

template<class T>
decltype(auto) log(T&& a) 
{
    std::count << a;

    return std::forward<T>(a); 
}


If we return a (possibly annotated) forward, and the return type is deduced (or we invent a attribute for that), then the call of the function is annotated instead.

Object(log(std::forward<T>(args))...);

// generates
// => Object(foo: log(foo: something));


When the rules are getting this complicated, you need to take a step back and ask "if it requires all of this complexity, is this really a good idea?" Bulldozer design of this type has a tendency to work out poorly and inflexibly. See the Coroutines TS.

Not to mention the fact that now you're telling people to write their code in a completely different way just to make logging continue working correctly. It seems wrongheaded to tell other people to change their code from the more natural syntax just to appease your new feature.

...

In fact, I just realized that `std::forward` itself wouldn't work, since its argument does not have the corresponding name. So how can you call it with a named parameter?

And if you special-case this as some magical property of `std::forward`... what happens if people use the equivalent `static_cast`? Or is that special-cased as well?


We can declare reference collapsing actually forces the compiler to add annotation, then use the above new rule to create std::forward itself.

So, what about `forward_as_tuple` and `std::apply`? We ought to be able to pass named parameters through `std::async` and similar functions, right?

If this is going to work, you need to lay down exactly what the rules are for this forwarding of named parameters.

Absolutely. But it is very important both to be errors and to work with it.

You have that backwards. If the rules don't allow them to be errors without breaking something, then they can't be errors. You need to make sure that a set of rules can be established before you can say that name mismatches must be errors.

To work is important, because this is standard and recommended way of creating objects, not just some minor library helper.

To be an error is important because A) It is breach of contract in basically all cases to get the arguments wrong.

The "contract" in C++ function calls is defined by our strong typing system. A contract is violated when you provide incorrect types to a function. This means that the contract visible at all levels of the code. If you're in function A, and you call function B which passes your parameters to function C, then we can see that.

If you want name mismatches to always be a "breach of contract", then that contract needs to be part of the actual type system at all levels, not some external channel of information that insinuates itself magically via unknown means. If it's important enough to truly be part of the "contract" of a function, then it's important enough for the name to be required, not optional.

If all the name is for is to visually indicate something, on the level of a comment, then building a system of rules making it an error is wrong.

B) Nowadays we have very aggressive static analyses which are great but tend to generate false positives - warnings might go unnoticed.

P0671's rules for it wouldn't generate false positives; it generates false negatives: places where there is a mismatch, but the compiler doesn't see it.

About macros. I believe names will not break on user-defined macros, because the compiler should not look for macros when he sees something:

That's not how the preprocessor works. `something:` is an identifier token followed by a `:` token. If that identifier token names a macro, then the preprocessor will convert it into something else.

This feature is not important enough to go changing the preprocessor/tokenizer. Especially since that would break labels.

mihailn...@gmail.com

unread,
Aug 2, 2018, 2:25:03 PM8/2/18
to ISO C++ Standard - Future Proposals, floria...@gmail.com, mihailn...@gmail.com
You either support named arguments or you don't. If your library does not support named arguments, the users will not be using named arguments with your library.
Once the library is updated, be it by adding names or adjusting some code if needed, then the users can use names.

About pass-through, it all depends if we decide it is a good idea.
 

...

In fact, I just realized that `std::forward` itself wouldn't work, since its argument does not have the corresponding name. So how can you call it with a named parameter?

And if you special-case this as some magical property of `std::forward`... what happens if people use the equivalent `static_cast`? Or is that special-cased as well?


We can declare reference collapsing actually forces the compiler to add annotation, then use the above new rule to create std::forward itself.

So, what about `forward_as_tuple` and `std::apply`? We ought to be able to pass named parameters through `std::async` and similar functions, right?


Out of question. Names are either direct call or forwarding reference call where the compiler is our friend. If one passes names to async it will error out when they are forwarded to the thread.

And no need to go to thread and tuple, even function pointers don't have names and error out.


If this is going to work, you need to lay down exactly what the rules are for this forwarding of named parameters.

Absolutely. But it is very important both to be errors and to work with it.

You have that backwards. If the rules don't allow them to be errors without breaking something, then they can't be errors. You need to make sure that a set of rules can be established before you can say that name mismatches must be errors.

 In many places it is an error to pass a name, not because there is a mismatch, but because this is not supported. 
And as Henry Miller argued - better to not support them in some places then to silently ignore them!



To work is important, because this is standard and recommended way of creating objects, not just some minor library helper.

To be an error is important because A) It is breach of contract in basically all cases to get the arguments wrong.

The "contract" in C++ function calls is defined by our strong typing system. A contract is violated when you provide incorrect types to a function. This means that the contract visible at all levels of the code. If you're in function A, and you call function B which passes your parameters to function C, then we can see that.

If you want name mismatches to always be a "breach of contract", then that contract needs to be part of the actual type system at all levels, not some external channel of information that insinuates itself magically via unknown means. If it's important enough to truly be part of the "contract" of a function, then it's important enough for the name to be required, not optional.


Incorrect argument places is breach of contract. Can we do this using types on any scale - no. A math library or a physics library, written by physicists-that-know-how-to-program, will never go all types.
It is not practical, probably nice, but not practical and is not always semantically sane to say the least. Yet, passing n into k is breach of contract we don't land on Mars.

Should names travel across all wrappers, absolutely, if we can do that. But we can't. We do as best we can. 90% of people that need named arguments are happy with just direct function call support anyway.
We simply need to decide where to draw the line.

 
If all the name is for is to visually indicate something, on the level of a comment, then building a system of rules making it an error is wrong.

B) Nowadays we have very aggressive static analyses which are great but tend to generate false positives - warnings might go unnoticed.

P0671's rules for it wouldn't generate false positives; it generates false negatives: places where there is a mismatch, but the compiler doesn't see it.


Then it is worse then I thought. One is better of with comments then, at least there is no false sense of security. 
 


About macros. I believe names will not break on user-defined macros, because the compiler should not look for macros when he sees something:

That's not how the preprocessor works. `something:` is an identifier token followed by a `:` token. If that identifier token names a macro, then the preprocessor will convert it into something else.

This feature is not important enough to go changing the preprocessor/tokenizer. Especially since that would break labels.


Well, one more argument to have names just on few selected arguments, when it comes to the standard library. They will be part of the standard and the user will have to deal with it.

schreiber...@gmail.com

unread,
Aug 2, 2018, 5:40:12 PM8/2/18
to ISO C++ Standard - Future Proposals, floria...@gmail.com, mihailn...@gmail.com
On Monday, July 30, 2018 at 7:58:25 PM UTC+1, mihailn...@gmail.com wrote:
It is an error to pass a name to an argument that it is not declared to have name.

void func(int arg);

// user

func(arg: 5); //< error

There is one exception. If, and only if, the argument is forwarding reference and it is forwarded,
then the forwarding function is allowed to be called with an argument name, even if one is not declared for that argument.

5. Because names are purely compile time, and perfect forwarding is always inline, names are able to change call sites.

This is done by the compiler alone, by prepending calls to std::forward with the name, used on the forwarded argument, for the call of the enclosing function.

void func(int& foo: arg);

template<class T>
void fwdfunc(T&& arg) //< no name for the argument
{  
  func(std::forward<T>(arg));
}

// user

int val = 5;
fwdfunc(foo: val) //< allowed

// generates

void fwdfunc(int& && arg) 
{  
  func(std::forward<int&>(arg)); 
// =>  func(foo: std::forward<int&>(arg))
// =>  func(foo: int&)
}

If the forwarding function declares a name for that argument, the user specified name is checked against that name instead and std::forward is called normally.
 
I agree with Nicol that creating special rules just for forwarding (and making std::forward<> a special case) feels like the wrong approach. Fixing the problem of forwarding is important though, and it may require some more work... Perhaps a more generic mechanism. For example you could think of allowing parameter names to be templated string constants:

template<constexpr_string N>
void foo(int N : param) {
    log
(param);
    bar
(N : param);
    bob
(value : param);
}

This would effectively create a function foo() that accepts any name for its first argument. It can then forward that name to bar(), or use no name at all for the debugging log() function, or use a different name when calling bob(). This may seem to contradict the rule that a function can only have one name defined for its parameter, unless you see foo<"mother"> and foo<"width"> as different functions (which they are). The perfect forwarding idiom then becomes:

template<typename T, constexpr_string N>
void foo(T&& N : param) {
    bar(N : std::forward<T>(param));
}

Of course, that becomes close to making names part of the type system in some sense. But indeed I'm not sure there's a way to implement forwarding without it.
Reply all
Reply to author
Forward
0 new messages