Fun with C++26 reflection: Keyword Arguments
146 comments
·February 10, 2025gpderetta
If you are willing to use macros anyway, you can make the following work in C++ today:
foo($(bar)=10, $(baz)="hello");
In fact you could 10 years ago when I implemented it[1]; in fact it allows significantly more than just named arguments (named tuples!), but please, consider it as some sort of art and not really something that should be anywhere close to production.[1] https://github.com/gpderetta/libtask/blob/a5e6e16ddc4e00d9f7...
feverzsj
There are tons of more insightful examples in the proposal[0]. C++26 reflection could replace most meta programming tricks, though the syntax isn't the most pleasant.
p0w3n3d
foo({.x=2, .y=2})
I remember using this syntax in C in my 2017 project. This is very clear to call methods like that, I used it with minor #defines, i found the inspiration in the book "21st century C."sagarm
The post mentions this obvious solution before delving into madness.
harrison_clarke
i believe this is standard since c99
you do have to cast it, though: `foo((vec2){.x=2, .y=2})`
the oldest MSVC on godbolt also accepts it, so it should be very portable as long as you're not using some embedded compiler from the mid 90s
a related fun thing is that you can pass it by pointer, and the lifetime will extend until the function is done:
`foo(&(vec2){ .x=2, .y=2 })`
p0w3n3d
Smells like UB. Is this defined in the standard?
harrison_clarke
C99 spec, section 6.5.2.5: https://www.open-std.org/jtc1/sc22/WG14/www/docs/n1256.pdf
> 6. The value of the compound literal is that of an unnamed object initialized by the initializer list. If the compound literal occurs outside the body of a function, the object has static storage duration; otherwise, it has automatic storage duration associated with the enclosing block.
it's valid until the end of the block. so, if the function returns the pointer, you can even use that until the end of the block
there's actually an example in the spec, on that page:
> drawline(&(struct point){.x=1, .y=1}, &(struct point){.x=3, .y=4});
nialv7
I am scared by what C++ people think is fun.
bluGill
What do you do with your Friday nights?
I rewrote the boot sector on a floppy drive in raw machine code (not assembly, I wrote the bytes by hand) before trying to see how I could abuse C++.
I consider the above normal engineering/hacking and wonder why someone who wouldn't think the above is fun is doing reading hackernews.
badmintonbaseba
It's mostly C++. You are right to be scared.
BodkinsOdds
Needing that MakeArguments macro makes this substantially worse than just defining an aggregate struct for your arguments and using designated initializers. I've never wanted to reorder named arguments anyway, I've only ever wanted to elide some of them.
knorker
I love C++. I've coded in C++ for like 30 years at this point. C++11 breathed new life into the language, to the point where it's a different and much better language now.
But some of these new features… I feel like they're a bit desperate attempts at fitting in with the kids.
Not to start a language war, but I don't see how any attempt at stapling modern features onto C++ make it a good choice in 2025. There are at least two viable plug in replacements, that have good interop.
Like I said, I've coded C++ for 30 years (along with other languages, sure), so I'm not a fad follower. I don't say it lightly, but I do say that coding C++ in 2025 means creating technical debt. And these features won't change that.
oilkillsbirds
Poor, poor C++... cries in C
anitil
I've seen cases where people do things like
my_func(/*arg1=*/val1,/*arg2=*/val2)
And I suppose you could write a validator to make sure that this worked. Or using an anonymous structure in C99, or a named structure in C89. And of course a pointer if you care about register/stack usage etc.I'm not sure what the other options are.
wffurr
>> I suppose you could write a validator to make sure that this worked
No need to write it:
https://clang.llvm.org/extra/clang-tidy/checks/bugprone/argu...
Efb
anitil
That's great, I wish I'd known about this ages ago
tjalfi
C++20 added designated initializers, so they're also an option.
my_func({.arg1 = val1, .arg2 = val2});
anitil
Oh I thought that was a C99 addition? It's been a while since I've used them.
Edit: struggling to find a source, though this GCC doc suggests it's C99 (but maybe that's only GCC?) - https://gcc.gnu.org/onlinedocs/gcc/Designated-Inits.html
pragma_x
Even though you need to declare a struct for this (or other) functions to receive these fields, I feel like this is the cleanest approach. What I'm not sure about is if reference, value, or pointer is the best way to let the compiler optimize this.
brandmeyer
> And I suppose you could write a validator to make sure that this worked.
Like this one!
https://clang.llvm.org/extra/clang-tidy/checks/bugprone/argu...
anitil
Wow I had no idea, that's great!
ranger_danger
at that point why not just pass in a struct as your argument
anitil
I suppose it depends on your application. I worked in embedded devices where stack was limited and the compiler was .... less than reliable about how it would manage passing a large struct to your function, which is why I mentioned passing in a pointer instead
jcelerier
depending on the platform this may cause things to be pushed / poped on the stack instead of being passed as registers
thom
C++ reflection is now good enough that hopefully we’ll start to see more game engines using it to work out components and properties instead of weird macros. jcelerier’s work on things like Avendish really does feel quite fresh and modern, which is not my usual reaction to C++ frameworks. Obviously it’s lagging a good 20 years behind C# et al but we’ve come a long way since IUnknown.
the_hoffa
I'm not sure if you mean to have a /s in there, but personally I never really liked reflection.
C# had it but it was also in part because it interop'd with .NET which had C++.NET, VB.NET, F#.NET, VBScript.NET, ASP.NET, Core.NET, Web.NET, Net.NET and so much more .. reflection was an "easy" way to have other dev's interact with each others code as a type of "contract".
I really like C# and what it can do, but having to check if a particular method exists within an external dependency is in part what lead to "dll-hell" .. it's the antithesis to an API and a "software contract" .. honestly it feels like C++26's "reflection" is more an answer to the ABI issue that has plagued C++ since its inception.
If C++ really wants to help "game-engines" or make actual strides, then it should add basic GUI support to the language itself. That'd kill off 90% of the other framework/libraries out there.
As with other parts of the language, you don't -have- to use it .. and since it's trying to be the next Java in it's eternal update path, why not add GUI support at a language level ??? Hell the std::thread just calls pthread_create or CreateThread under the hood anyways, just with a 15+ high stack frame .. why not add GUI!?
spacechild1
> If C++ really wants to help "game-engines" or make actual strides, then it should add basic GUI support to the language itself.
This feels like a total non-sequitur. What does GUI have to do with component and property systems?
> why not add GUI!?
Because that is 100% out of scope for the C++ standard library. Also, I don't even want to imagine what a GUI library designed by committee would look like...
the_hoffa
Agree it's a non-sequitur; as is any idea of "game-engine" code in C++ (as the post I replied to mentioned), hence the argument.
And 100% out of scope of C++, like the std::thread?? I've worked on many an embedded system that has no concept of threads, yet the C++ committee decided to add the std::thread as a part of the STL in C++11 instead of agree on a standard ABI. So why not GUI's, or sockets, or any other more "common" modern idiom?
If you don't want to imagine what a GUI library designed by committee would look like, I'd argue what a language like C++ looks like designed by committee (6 versions in 10 years).
kllrnohj
> If C++ really wants to help "game-engines" or make actual strides, then it should add basic GUI support to the language itself. That'd kill off 90% of the other framework/libraries out there.
Find me a single language that has an included GUI that anyone uses. I'll wait.
Even VB.NET, for which building GUIs was its entire reason to exist at all, has multiple GUIs officially, they couldn't even stick to one.
This is absolutely a terrible idea for a language, any language, to engage with at a language / standard library level.
pjmlp
Smalltalk-80 when it was at Xerox PARC, Objective-C at NeXT, VB (native version, not .NET), Hypercard.
prabhu-yu
I liked the syntax of key word arguments in Python. Then asked myself on how come this is not popular in C/C++ world. After seaching internet, found the way and documented here.
https://prabhuullagaddi.substack.com/p/simulation-of-keyword...
maleldil
IIUC, the very first option (designated initialisers with custom structs per function) is how Zig does it, and it seems to work well enough there. It's verbose, so I wouldn't use it for everything, but it doesn't seem all that unreasonable.
pipeline_peak
Reflection always seems like a convenient way for programmers to write less code at the cost of performance overhead. Also the type of trivial code that could be easily generated by an LLM anyway.
Things like serializers and MVC event bindings come to mind.
This is all, as the title suggests, good fun, but I wouldn't use it for any real code. Instantiating the parameter struct on a separate line adds only a small amount of extra boilerplate at the call site, in return for which it's a million times easier to read than any of these tricks.
Three lines instead of one seems like a lot of overhead, but in practice you would only bother for a function that takes loads of arguments so the extra overhead is really much smaller.----
Smaller points:
Are the fields in FooArgs really initialised if they're not explicitly set? I can believe they are, after all it's brace initialisation. But IMHO that code isn't super obvious. I'd be more comfortable if they had default member initialisers, i.e., "int x = 0; int y = 0;" in FooArgs. In my version above, you really do need these (unless you remember to brace initialise).
It took me a while to see why they bothered to have a string template parameter for TypedArg in the first usage. It prevents mixing up two arguments: if TypedArg didn't have that, then you could call foo(y=3, x=2) and it would compile but have the effect that the parameter x would be 3 and y would be 2.