Named Parameters in C++20
pdimov.github.io
pdimov.github.io
It's a pretty convincing fake, but it has one problem: the person who wrote the function needs to have written it that way. What I wish more languages had (C++, Java, etc.) is the following:
Suppose I write some function that take way too many parameters:
void foo(char a, short b, long c, float d, double e){}
What I want, as a caller, is for the language to let me get a struct corresponding to the argument list, something like this:
foo::args whatever;
whatever.e = 3.14;
// fill in the rest
And then add a bit of magic syntax so we can "unpack" that struct to actually call foo. (Extending this proposal to handle varargs is left as an exercise to the reader, because I have no idea.)I see two possible implementations:
- either the struct is replicated at the function call (not really efficient, but the semantics are straightforward);
- either the struct is used to prepare directly the stack for the function call (more efficient, but the semantics must be defined).
One of the issues of the second approach is that if the structure/variable is a classical one, what should happen if we use the variable after the call?
If we have to read the values inserted before the call then we have to keep a copy of the structure in the stack (so it's the same as the first approach). Or we can consider that the variable no longer exists in the scope. Which corresponds to the ownership concept in Rust for example. (but, to my knowledge, this does not exist in C++)
What is the best option? Or are there other options?
Another question, what sizeof(foo::args) should return ?
struct foo_args{char a; short b; long c; float d; etc...}
and then asked for sizeof(foo_args).
From the first proposal the compiler can directly prepare the stack for the call.
As in this example: https://godbolt.org/z/f17szP
It might be a bit verbose, but I think I would still prefer this over native syntax for unpacking.
1. I need to handcraft the tuple type myself, and make sure it matches the function declaration. (In other words, I don't want to manually write tuple<char, short>. I want to write foo::arg_tuple.)
2. This provides no support for named parameters. Tuples in C++ are numbered, not named.
In general, it'd be really nice to have compile-time access to compile-time function info like this. Arguments, their types, etc.
Also they can lead to a poor pattern which is functions/methods that take too many parameters.
They also tend to make generics harder to understand as the possible parameters can vary. Of course you can make the same problem in C++ via, say, `auto multiply(int x, int y)` and `auto multiply(double x, double y, enum rounding rp = rounding::nearest)`
In general they impose the verbosity (in keyboarding and reading) in the routine cases but don’t free you from having to look up the argument semantics of lesser-used functions.
Side point: I used “keyboarding” because the obvious word, “typing” has a homonym which made the sentence confusing!
I understand how this is a problem but I’ve seen more than one function in C that takes 4 or more parameters.
Methinks it’s more a product of being an inexperienced coder and not being able to model a domain appropriately. The huge benefit? If you do need 4 parameters you can at least read them / understand them if they are named!
Would love to see you flesh this out a bit more
CreateProcess(NULL,cmd,NULL,NULL,TRUE,CREATE_NO_WINDOW,NULL,NULL,&si,&pi);
is a terrible interface that we should try to split up better, but CreateProcess(
lpApplicationName: NULL,
lpCommandLine: cmd,
lpProcessAttributes: NULL,
lpThreadAttributes: NULL,
bInheritHandles: TRUE,
dwCreationFlags: CREATE_NO_WINDOW,
lpEnvironment: NULL,
lpCurrentDirectory: NULL,
lpStartupInfo: &si,
lpProcessInfo: &pi,
);
tends not to attract the same scrutiny for some reason. On the other hand, removing named parameters doesn't actually fix this (see jasode's example at https://news.ycombinator.com/item?id=24401913 so I don't need another dozen lines), so it's not clear that this is a good argument against named parameters.I basically emulate this in other languages, like Rust, with an “options” struct that has default values.
And in fact in that case using a keyword isn’t that different from binding the meaning into the function name (`divide_rounding_down` and so on).
But in my experience I saw a pernicious pattern: that the function name would essentially become the common entry point for a large number of divergent functions `divide(dividend: x, divisir: y, truncate: true, underflow_handling: ufh::throw, negative_permitted: true)` and so on (contrived example for explanatory purposes).
In addition, positional arguments are simply easier to read; as keyword arguments are not ordered, you’re essentially parsing the arguments, seeking the important ones etc. You’re increasing the cognitive overhead of the common case while only marginally assisting the uncommon case. It’s like reading while hearing the sound of the words in your head: quite possible, but significantly slower than having the meaning enter directly.
>In addition, positional arguments are simply easier to read; as keyword arguments are not ordered, you’re essentially parsing the arguments, seeking the important ones etc.
Microsoft's raw Win32 API SDK is based on the C Language and even though that language doesn't have named parameters, it still did not discourage functions designed with lots of parameters that's difficult to mentally parse.
To make such code self-documenting, this is how I typically write Win32 API calls to make the code readable:
ret = CreateProcess(NULL, // _In_opt_ LPCTSTR lpApplicationName,
cmd, // _Inout_opt_ LPTSTR lpCommandLine,
NULL, // _In_opt_ LPSECURITY_ATTRIBUTES lpProcessAttributes,
NULL, // _In_opt_ LPSECURITY_ATTRIBUTES lpThreadAttributes,
TRUE, // _In_ BOOL bInheritHandles,
CREATE_NO_WINDOW, // _In_ DWORD dwCreationFlags,
NULL, // _In_opt_ LPVOID lpEnvironment,
NULL, // _In_opt_ LPCTSTR lpCurrentDirectory,
&si, // _In_ LPSTARTUPINFO lpStartupInfo,
&pi); // _Out_ LPPROCESS_INFORMATION lpProcessInformation
If the language had named parameters, I wonder if Microsoft would have used them so programmers wouldn't have to sprinkle extra comments like that. (Comments are also brittle because they're not real tokens that the compiler would check.) Without the comments, it looks like this: ret = CreateProcess(NULL,
cmd,
NULL,
NULL,
TRUE,
CREATE_NO_WINDOW,
NULL,
NULL,
&si,
&pi);
All those naked NULLs and TRUE params are very hard to parse unless you've memorized the positions.I'm not recommending that we need the verbosity of Objective-C named parameters but I don't see how mentally relying on position is less cognitive load.
Sure, I understand that too many parameters is to be avoided and can point to some underlying poor architecture design. However, _if_ there's irreducible complexity that calls for 5+ parameters to minimally specify behavior, what are the better syntax options than named parameters?
If you say "create a struct with members and pass the struct instead", then you're just shifting the "named params" to the "struct member names".
This causes the extra verbose syntax of defining one-time-use struct variables to pass as a function argument. In contrast, named parameters in the function can act as "anonymous unnamed struct" which is cleaner.
Using structs as function parameters is more useful when the structs can also be re-used elsewhere in multiple places (e.g. 3d points {x, y, z}).
-onStream:writeObject:withKey:;
For example, suppose you have a function foo and within foo you call another function baz, where baz has a very large range of customization parameters. If you want to memoize calls to foo with a decorator, you can just pass kwargs for any “pass through” customizations of baz. The memoizing decorator can flatten kwargs out into a tuple along with any positional args, so you get call signature memoization without needing to restrict or list out all possible args of baz. In fact you can make this perfectly generic over the function being memoized so it doesn’t matter if you change baz to bar one day, with a whole new signature.
You can combine this with the inspect module and with functools.wraps to ensure full and complete docstrings and introspected signatures as well, so that putting kwargs all over doesn’t hurt readability, remove info from docstrings or help messages, or make it hard to read.
Interesting use case, thanks for sharing! Out of curiosity, when are some scenarios where it's "critical" to write code this way? I mostly interact with small pytorch / numpy codebases that don't grow large enough for this sort of thing.
Even in hardcore numerical computing this way of working has huge wins over traditional OOP designs or plain flat function designs like C. For example see numba’s jit and autojit decorators that also work this way.
For cases when you want to do it yourself, think of functions that wrap complex matplotlib or seaborn plotting utilities that rely on tons of optional parameters via kwargs. I often find it super helpful to do this when wrapping pandas functions too, which also involve tons of kwargs customization.
FWIW I work professionally in deep learning and other ML. I think scientific computing in Python in particular highlights the importance of this.
But those are non-trivial or cumbersome in their own way, and often people don't for their own internal functions. Named parameters neatly side-steps the issue entirely, by rolling the "local constant" right into the function call itself.
Like consider these two python snippets:
['Ford', 'BMW', 'Volvo'].sort(true)
vs. ['Ford', 'BMW', 'Volvo'].sort(reverse=true)
The latter is much more readable than the former, and there wasn't any "too many parameters" issue. It's a way to help create self-documenting code that's also compiler enforced. And it absolutely frees the reader up from needing to go lookup what the boolean parameter on "sort" means.For the above you'd have an 'rsort' or 'reversesort' function instead (or if your language supports it, some fluent kind of sort().reverse()). If you have a lot of options, some of them boolean, you might send in an options struct or have an enum as mentioned in the article.
list.sort(true /* reverse */);
This convention is very handy for certain Win32 functions that take lots ofparameters, such as CreateProcess or CreateWindowEx. ['Ford', 'BWM', 'Volvo'].sort(`reverse)
TypeScript can do that with strings: https://www.typescriptlang.org/docs/handbook/literal-types.h... enum SortDirection { FORWARD, REVERSE }
sort(direction : SortDirection = SortDirection.FORWARD) { ... }
['Ford', 'BMW', 'Volvo'].sort()
['Ford', 'BMW', 'Volvo'].sort(SortDirection.REVERSE) var lst = someLst();
lst.sort(Comparator.reverseOrder());1. Clearly communicates the purpose of the variable
2. Means that order of parameters is not important
3. Makes the internal interface largely compatible with its JSON web service interface
It's been about 10 years since I coded any Objective-C and I do agree that syntax was an oddbal.
Never seen that as a tendency in languages with named params.
I have, however, seen functions / methods in languages without named params that take too many arguments and one has to guess what they are...
function foo({arg1, arg2, arg3}:{arg1: Thing1, arg2: Thing2, arg3: Thing3) {
...
}IMHO this sort of named arguments "easter egg" is fine for big "option bag structs" passed to functions that are not performance critical (or for small struct <= 16 bytes). For other cases, regular args are better.
OTH it seems like passing normal args also spills over into the stack very soon (only 4 registers used?):
https://www.godbolt.org/z/Mz9MTG
The main difference seems to be that normals arguments "spill over", while passing structs by value puts everything on the stack once the threshold size is passed.
In any case you don't get to actually see the passing at the callsite because the result `45` is inlined into `main()`. :)
It's more complicated than that - floats/doubles and structs of them are put in SSE registers instead. Mixed structs are packed into integer registers (!) if they are small enough.
And with C++ classes with non-trivial destructor/etc. are always passed on the stack.
However, if you use an lvalue expression then yes, the copy constructor is invoked (in `bar` here), and you can avoid that by using an xvalue expression (in `baz`, using `std::move`): https://godbolt.org/z/MnKnWM
Basically it doesn't depend on being passed through a struct as much as on the parameter itself. You'd see the same behavior if the `C` struct was passed as a regular parameter.
> If the size of an object is larger than eight eightbytes, or it contains unaligned fields, it has class MEMORY
> [...]
> If the size of the aggregate exceeds a single eightbyte, each is classified separately. Each eightbyte gets initialized to class NO_CLASS.
So that only happens if the whole structure is bigger than 64 bytes. Otherwise everything gets passed in registers like normal.
> If the size of the aggregate exceeds two eightbytes and the first eight-byte isn’t SSE or any other eightbyte isn’t SSEUP, the whole argumentis passed in memory.
However in my experience there are human factors problems which I posted in another comment.
Structs with more than one field don't fit into a register.
It makes everything so readable. I can come back to my code a few weeks later and I don’t really need to look up function signatures.
In particular, my favorite thing about this in Objective-C is that the C functions have a different way of being called so you can easily distinguish between the differences in paradigms.
I wonder what other languages force all calls to be via named parameters. Would love to have another option :)
I guess in Python you could l’ont your codebase to require keyword parameters or opt-in positionals.
open(/port/ 127)
a) have default parameters in any parameter position. I.e. for this to be legal:
foo(int x = 0, int y, int z = 0) {}
b) to allow me to invoke a function like this: foo(,) //x and z are passed as default values:
c) or like this using named parameters: foo(x = 5, y = 1,)Also, some might argue that what you're describing (call sites not becoming invalid when you update the function) is a feature, not a bug. This way, you can add options to your functions and still have everything compile like it should.
I wouldn't personally argue that though, i think it leads to bad practice where functions take a bazillion options and they become horrible tech debt. I'm working in a mixed C++ and Lua codebase, and the Lua codebase is littered with functions that take, like, 9 different arguments that change the behavior of the function. When I asked one of my colleagues about this style, he said it was one of his favorite features of Lua, that you can add arguments to a function to change the behavior in certain cases but keep the original functionality (guarded with default values or if's or whatever). It's made much of the code a living nightmare!
So, I guess, in the process of writing this comment I've come over to your side :) say no to adding arguments with default values!
But I'd definitely like to be able to say this one isn't optional or implicitly default constructed (or worse uninitialized, maybe that is already precluded by any amount of brace initialization, or maybe not if the class is POD, I'll have to check).
Someone suggested below deleting default constructors, you could make a template class that did that and forwarded other constructors, but I'd still like to be able to allow explicit default initialization in the ones that aren't considered optional.