C++26 reflection at compile-time
In today's post, I like to talk about C++26 and one of the probably most impactful features that have been added to the working draft. While C++26 is still some months away from official completion, since the WG21 summer meeting in June we all now know what will be in C++26.
While the new standard will have plenty of great improvements the one that will most likely change a lot is reflection at compile-time! In Sofia we voted seven reflection papers into C++26:
- P1306R5 Expansion statements
- P2996R13 Reflection for C++26
- P3096R12: Function parameter reflection in reflection for C++26
- P3293R3: Splicing a base class subobject
- P3394R4: Annotations for reflection
- P3491R3: define_static_
- P3560R2: Error handling in reflection
The papers above should give you enough to read for your vacation. I'll leave that theoretical study up to you for now.
Let's talk practical
The main question is, what can you do with that new feature? Well, I'm not the first one who published their ideas.
Steve Downey has an example that parses a JSON string at compile-time creating C++ objects out of it. The direct Compiler Explorer link is godbolt.org/z/YsEK418K6.
The second example comes from Jason Turner which allows you to generate binding to other languages. The direct Compiler Explorer link is godbolt.org/z/6Y17EG984.
While I find both examples great I wanted to show you mine. The issue I tried to solve for years, which also come up in various training classes I thought and even in my own book (Programming with C++20 - Concepts, Coroutines, Ranges, and more) I had to swallow the bitter pill of showing, well not so great code.
Reflection, reflection on the wall, what can I do with you all?
What I'm talking about? Enums! Oh no, not how to convert an enum to a string and vice versa. By the way, code for doing that is in that pile of papers above.
No I have at least one other issue which enums: iteration. How often have I wanted to iterate over an enum. Sure there are solutions, mostly macro based and with a lot of rules. For example, only consecutive numbers and a final member called something like Last or MAX. But what when there are holes in an enum? Like
1 | |
Right, that's when the rule kicks in that non-consecutive numbering isn't allowed. Not any more!
Let's make that work, much like in other languages like C# where iterating over the enum values is possible. Without further ado, here it is:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | |
I created a helper variable in A, simply because obtaining the number of enum values in a enum is helpful on its own.
The implementation for the task itself is then in B. You can come up with a different implementation more suitable for lager enums, but this one is sweet and short.
The utility function in action doesn't look special, you can't tell that reflection is going on under the hood:
1 2 3 4 5 | |
You can see, that B returns the strongly typed enum value, that's why the std::to_underlying is required when using the value with std::print. This is a design decision. I decided to stay strongly typed as long as possible.
There are more design considerations, like should get_enum_values be a variable as well? It is constant for each type after all.
At this points I'm not going to explain all the new parts. I just wanted to show you what is possible now.
If you want to play with the code here is the full Compiler Explorer link compiler-explorer.com/z/W8b9xrx5j.
Feel free to reflect a bit on reflection :-)
Andreas
P.S.: In case you wonder whether the implementation of B is correct for an empty enum which gives a zero-sized std::array, the answer is, yes, the code is well formed. One of the advantages of std::array is that there exists a special case for the zero-sized case. A C-style array would be not valid.
