C++ 26 brings us a lot of cool stuff with reflection. I’m using it in a project of mine where I have hundreds of packet schemas for Minecraft’s network protocol.
NOTE: This blog post is more of a show-and-tell than an instructive “How to reflection”, a guide to C++ reflection is coming soon and will be accessible here
Here’s two real neat thing I found you can do. Both examples are working on GCC16.1 with Clang support to hopefully come soon.
1) Automatic type-based schema (de)serialization
Instead of having to specify
export struct handshake_s {
std::int32_t protocol_version;
std::string server_address;
std::uint16_t port;
std::int32_t intent;
static coro::task<handshake_s> read(game::connection *connection);
};
coro::task<handshake_s> handshake_s::read(game::connection *connection) {
[[maybe_unused]] const auto [length, id] = co_await parse_packet_header(connection);
co_return handshake_s{
.protocol_version = co_await connection->read_var_int(),
.server_address = co_await connection->read_string(),
.port = co_await connection->read_u16(),
.intent = co_await connection->read_var_int(),
};
}
and write out the entire read (or write) function to de(serialize) things to the wire. I can specify the type once, and let reflection do the member iteration for me.
Here is some example code that prints the member variables and their values with C++ 26 reflection.
template <typename T>
void print_types_and_values(const T& obj) {
constexpr auto ctx = std::meta::access_context::current();
constexpr static auto members =
std::define_static_array(std::meta::nonstatic_data_members_of(^^T, ctx));
template for (constexpr auto member : members) {
std::print("{}: {} = {}\n",
std::meta::identifier_of(member),
std::meta::display_string_of(std::meta::type_of(member)),
obj.[:member:]
);
}
}
Exending this to do automatic parsing for each member based on the type is left as an exercise to the reader.
2) Type Synthesis
C++ 26 reflection also somewhat silently adds a very powerful feature. Type synthesis, allowing you to programtically create types from thing air. This includes creating types from json (Which inspired this section of the blog).
Creating a type from scratch in C++ requires a lot of steps. Let’s go through them with the goal of being able to make a simple type similar to.
struct {
int a;
float b;
std::string hello;
};
Where we can make the types / names whatever we want. It is not a goal of this blog post to handle nested types, but this can be derived from the existing material.
Synthesising a type
We need a way to be able to go from member variable definitions to an actual usable type, the following code accomplishes this 1.
template <std::meta::info ...Members>
struct outer {
struct inner;
consteval {
define_aggregate(^^inner, {Members...});
}
};
template <std::meta::info ...Members>
using synthesis_type = outer<Members...>::inner
This code is a utility that allows us to go from a std::vector<std::meta::info> to a struct with the member info types.
consteval std::meta::info create_type_info() {
auto members = std::vector<std::meta::info>();
auto dms = std::meta::data_member_spec(^^int, {.name ="a"});
members.push_back(std::meta::reflect_constant(dms));
dms = std::meta::data_member_spec(^^float, {.name ="b"});
members.push_back(std::meta::reflect_constant(dms));
dms = std::meta::data_member_spec(^^std::string, {.name ="hello"});
members.push_back(std::meta::reflect_constant(dms));
return substitute(^^synthesis_type, members);
}
using synthesised = typename [:create_type_info():];
This code is the one that generates the member variable specifications. The second parameter which we use just for naming is std::meta::data_member_options, which looks like
struct data_member_options
{
std::optional</*name-type*/> name;
std::optional<int> alignment;
std::optional<int> bit_width;
bool no_unique_address = false;
std::vector<std::meta::info> annotations;
};
You now know a little bit more about C++ 26 static reflection and it’s abilities :)