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
enum class Color16 : uint16_t
{
};

struct RGB48 {
  Color16 red;
  Color16 green;
  Color16 blue;

  auto operator<=>(const RGB48&) const = default;
};

struct LEDState {
  bool  state;
  RGB48 rgbColor;

  auto operator<=>(const LEDState&) const = default;
};

struct LEDStateMessage {
  uint32_t messageId;
  LEDState state;

  auto operator<=>(const LEDStateMessage&) const = default;
};

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.

Book an in-house C++ training class

Do you like this content?

Here is your next chance joining my class:
  • Modern C++: When Efficiency Matters @CppCon
  • September 09 - 11, 2026, - UTC
Book your seat now!

What I want to achieve is something like this:

1
2
3
4
5
6
7
LEDStateMessage msg{.messageId = 3,
                    .state{.state = true,
                           .rgbColor{.red   = Color16{0x5u},
                                     .green = Color16{0x28u},
                                     .blue  = Color16{0x40u}}}};

Serialize(msg);

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
template<typename T>
requires(not std::is_pointer_v<T>) and std::is_trivially_copyable_v<T>
constexpr void Serialize(T& data)
{
  if constexpr(std::is_array_v<T>) {  A 
    for(int i{}; i < std::size(data); ++i) { Serialize(data[i]); }

  } else if constexpr(std::is_enum_v<T>) {  B 
    data = static_cast<T>(std::byteswap(std::to_underlying(data)));

  } else if constexpr(not std::is_class_v<T>) {  C 
    data = std::byteswap(data);

  } else {  D 
    static constexpr auto members =
      std::define_static_array(std::meta::nonstatic_data_members_of(
        ^^T, std::meta::access_context::current()));

    template for(constexpr auto& mem : members) { Serialize(data.[:mem:]); }
  }
}

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

Recent posts