In this tiny post I will briefly expose you to the C++26's upcoming reflection feature by sketching an extremely simple "serialization" skeleton.
As probably most people who follow the language's development, I am very excited for this feature. I tried to read the proposal and skipped through it to get some basic (syntactic, conceptual) understanding. I won't pretend... the proposal itself got scary in many parts but here we are...
As an exercise, I opted for a "son" serialization of basic types. "Son" because I'd love it to be Json (I am so funny right?), but it quite isn't due to time constraints and ergonomics. I was unable to get a compiler suite to experiment locally (apart form building from sources, I guess).
Tadaaaah (also on Compiler Explorer):
// x86-64 clang (experimental P2996)
// -freflection-latest -std=c++26 -freflection
#include <experimental/meta>
#include <vector>
#include <array>
#include <cassert>
#include <iostream>
#include <string>
#include <sstream>
#include <ranges>
#include <type_traits>
// Direct documentation snapshot:
// https://github.com/bloomberg/clang-p2996/blob/e130488ee5e3c0159d9bca705f63dbfe970485f6/P2996.md
// start 'expand' definition
// copied from
// https://github.com/bloomberg/clang-p2996/blob/e97e8c2eeab71377c4b1a5cbd54deb3c9a020c1e/libcxx/test/std/experimental/reflection/p2996-ex-parsing-command-line-options-1.sh.cpp
namespace __impl {
template<auto... vals>
struct replicator_type {
template<typename F>
constexpr void operator>>(F body) const {
(body.template operator()<vals>(), ...);
}
};
template<auto... vals>
replicator_type<vals...> replicator = {};
}
template<typename R>
consteval auto expand(R range) {
std::vector<std::meta::info> args;
for (auto r : range) {
args.push_back(std::meta::reflect_value(r));
}
return substitute(^^__impl::replicator, args);
}
// end 'expand' definition
// a modified copy of expand (above) that returns the size of the range
// a simple .size screams that nonstatic_data_members_of does not return a constexpr...
template<typename R>
consteval auto size_workaround(R range) {
return range.size();
}
template <typename T>
concept is_class = std::is_class<T>::value;
template <typename T>
concept printable = requires(const T& t) {
std::to_string(t);
};
template <typename T>
concept primitive_printable = !std::is_class<T>::value && printable<T>;
template <typename T>
requires is_class<T> || primitive_printable<T>
struct JsonSerialize {
// default implementation tries to perform std::to_string
// as you will see below, this implementation is meant as a fallback when T does not satisfy
// is_class concept
// this has issues: mishandling of pointer types, character types, ...
// but this just serves as a demo...
static auto serialize(T const& target) -> std::string { return std::to_string(target); }
};
template <is_class ToSerializeT>
struct JsonSerialize<ToSerializeT> {
// a specialization for "classes and structs"
// of course, a string, vector, ... satisfies this concept as well...
// this only demonstrates nested serialization for basic structs
static auto serialize(ToSerializeT const& target) -> std::string {
std::stringstream stringBuilder;
stringBuilder << "{";
constexpr auto member_count = size_workaround(nonstatic_data_members_of(^^ToSerializeT));
int counter = 0;
[: expand(nonstatic_data_members_of(^^ToSerializeT)) :] >> [&]<auto member>{
auto const& cur = target.[:member:];
constexpr auto memberType = std::meta::type_of(member);
stringBuilder << '"' << identifier_of(member) << "\":" << JsonSerialize<[:memberType:]>::serialize(cur);
counter += 1;
if (counter != member_count) {
stringBuilder << ", ";
}
};
stringBuilder << "}";
return stringBuilder.str();
}
};
template <>
struct JsonSerialize<std::string> {
// a specialization for std::string - yes, not generalized enough...
static auto serialize(const std::string& target) -> std::string {
return "\"" + target + "\"";
}
};
struct BAD {
std::string prepared_nested = "nested";
int* ptr_test = reinterpret_cast<int*>(0xDEADBEEF);
};
struct Y {
std::string prepared_nested = "nested";
char malicious_but_compiles = '\n';
};
struct X {
int age{999};
int salary{101231};
int body_count{-1};
std::string name = "name";
double thisWontLookOk = -0.0000001;
Y y_member;
};
int main() {
X testX;
std::println("{}", JsonSerialize<X>::serialize(testX));
testX.age = 69;
testX.name = "BogdanKVKV";
std::println("{}", JsonSerialize<X>::serialize(testX));
}
Its output:
{"age":999, "salary":101231, "body_count":-1, "name":"name", "thisWontLookOk":-0.000000, "y_member":{"prepared_nested":"nested", "malicious_but_compiles":10}}
{"age":69, "salary":101231, "body_count":-1, "name":"BogdanKVKV", "thisWontLookOk":-0.000000, "y_member":{"prepared_nested":"nested", "malicious_but_compiles":10}}
As you can see, I had to consult the Bloomberg's documentation and even examples (as I was unable to make the
proposal's examples compile)
EDIT - I am an idiot and should at least open the Compiler Explorer links before copying (outdated?) code from the proposal itself, they however do not use the template for
syntax - instead there is the expand
trickery as above.
(OF COURSE) It's got tons of issues, is not generic, bad code and whatnot but hey... I wanted to experiment, this is what I did, it compiles & runs and outputs a valid JSON for very simple values.
28th September 2024