Discover C++26's compile-time reflection

4 hours ago 1

Herb Sutter just announced that the verdict is in: C++26, the next version of C++, will include compile-time reflection.

Reflection in programming languages means that you have access the code’s own structure. For example, you can take a class, and enumerate its methods. For example, you could receive a class, check whether it contains a method that returns a string, call this method and get the string. Most programming languages have some form of reflection. For example, the good old Java does have complete reflection support.

However, C++ is getting compile-time reflection. It is an important development.

I announced a few months ago that thanks to joint work with Francisco Geiman Thiesen, the performance-oriented JSON library simdjson would support compile-time reflection as soon as mainstream compilers support it.

This allows you to take your own data structure and convert it to a JSON string without any effort, and at high speed:

kid k{12, "John", {"car", "ball"}}; simdjson::to_json(k); // would generate {"age": 12, "name": "John", "toys": ["car", "ball"]}

And you can also go back, given a JSON document, you can get back an instance of your custom type:

kid k = doc.get<kid>();

The code can be highly optimized and it can be thoroughly tested, in the main library. Removing the need for boilerplate code has multiple benefits.

To illustrate the idea further, let me consider the case of object-to-SQL mapping. Suppose you have your own custom type:

struct User { int id; std::string name; double balance; private: int secret; // Ignored in SQL generation };

You want to insert an instance of this user into your database. You somehow need to convert it to a string such as

INSERT INTO tbl (id, name, balance) VALUES (0, '', 0.000000);

How easy can it be? With compile-time reflection,  we can make highly efficient and as simple as single function call:

generate_sql_insert(u, "tbl");

Of course, the heavy lifting still needs to be done. But it only needs to be done once.

What might it look like? First, we want to generate the column string (e.g., id, name, balance).

I do not have access yet to a true C++26 compiler. When C++26 arrive, we will have features such as ‘template for’ which are like ‘for’ loops, but for template metaprogramming.  Meanwhile, I use a somewhat obscure ‘expand’ syntax.

Still the code is reasonable:

template<typename T> consteval std::string generate_sql_columns() { std::string columns; bool first = true; constexpr auto ctx = std::meta::access_context::current(); [:expand(std::meta::nonstatic_data_members_of(^^T, ctx)):] >> [&]<auto member>{ using member_type = typename[:type_of(member):]; if (!first) { columns += ", "; } first = false; // Get member name auto name = std::meta::identifier_of(member); columns += name; }; return columns; }

This function is ‘consteval’ which means that you should expect it to get evaluated at compile time. So it is very efficient: the string is computed while you are compiling your code. Thus the following function might just return a precomputed string:

std::string g() { return generate_sql_columns<User>(); }

Next we need to compute the string for the values (e.g., (0, ”, 0.000000)). That’s a bit trickier.  You need to escape strings, and handle different value types. Here is a decent sketch:

template<typename T> constexpr std::string generate_sql_valuess(const T& obj) { std::string values; bool first = true; constexpr auto ctx = std::meta::access_context::current(); [:expand(std::meta::nonstatic_data_members_of(^^T, ctx)):] >> [&]<auto member>{ using member_type = typename[:type_of(member):]; if (!first) { values += ", "; } first = false; // Get member value auto value = obj.[:member:]; // Format value based on type if constexpr (std::same_as<member_type, std::string>) { // Escape single quotes in strings std::string escaped = value; size_t pos = 0; while ((pos = escaped.find('\'', pos)) != std::string::npos) { escaped.replace(pos, 1, "''"); pos += 2; } values += "'" + escaped + "'"; } else if constexpr (std::is_arithmetic_v<member_type>) { values += std::to_string(value); } }; return values; }

You can now put it all together:

template<typename T> constexpr std::string generate_sql_insert(const T& obj, const std::string& table_name) { constexpr std::string columns = generate_sql_columns<T>(); std::string values = generate_sql_valuess(obj); return "INSERT INTO " + table_name + " (" + columns + ") VALUES (" + values + ");"; }

It is just one of many applications. The important idea is that you can craft highly optimized and very safe code, that will get reused in many instances. The code looks a bit scary, as C++ tends to, but it is quite reasonable.

In the coming years, many projects will be simplified and optimized thanks to compile-time reflection.

Code: I have posted a complete implementation in the code repository of my blog. I am sure it can be significanlty improved.

Read Entire Article