More C++26 reflection at compile-time
In today's post, I like touch-up on C++26's static reflection. In case you haven't seen, I wrote a first post C++26 reflection at compile-time a while ago.
One of the great things about reflection is that we can already explore the new feature since with Clang there is a compiler available that implements all the facets.
I was again exploring what of my use cases would be better solved with reflection. Now, one thing that I had to do most of my career is reading and writing data coming from a network connection. The definition of network was different at different times. What was common is, that everything going out was sent in network byte order (big-endian), and the everything that was received arrived in network byte order as well. For some systems there was no difference, as they where already big endian machines. But not all the time. Especially ARM pushed little endian. For a subset of the systems data had to be byte-swapped when it was received or sent.
How to swap data?
The issue here (was) is, how to swap the data? Every data type larger than a byte must be swapped every time. C++23 gave us std::byteswap which removes the need for the POSIX functions like htons. At least an improvement in terms of safety.
Let's start with an example. Below are the data structures I use today.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | |
As you can see, the last structure LEDStateMessage is later the root. This structure contains a structure, which itself contains a structure. So we are going recursive today.
All structs come with a spaceship operator, making verification easier.
What I want to achieve is something like this:
1 2 3 4 5 6 7 | |
The data should be swapped or better serialized in place. I assume a system with memory constraints where I cannot effort to spend twice the memory.
Now think for a moment about all the trouble you've been going through in the past to make Serialize work. I can tell, you, it would take a long time to share my pain.
Reflection to the rescue
Are you ready? Okay, here it comes:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | |
The implementation of Serialize contains four parts. In A, Serialize checks for an array. If the datatype matches, the implementation calls Serialize on each element of the array.
Next, in B, the implementation checks if the datatype is a enum. In that case, C++23s std::to_underlying is used together with std::byteswap to actually swap the bytes.
Now, in C, if that datatype is not a class type, std::byteswap is applied to the data.
So far so good. But that's all generic code that you can write before C++26. The tricky part are the data members of class types. Which is what is handeled in D.
Thanks to the reflection capabilities, the implementation iterates through all the data members of the class calling Serialize on them. Et voila, an excellent first draft of a serialize function.
Andreas
