BohdanQQ

Trying out C++26 Reflection

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.

It ain't much, but it's honest work

Last update

28th September 2024