×
Community Blog How to Deeply Use yaLanTingLibs Compile-time Reflection Library in Development

How to Deeply Use yaLanTingLibs Compile-time Reflection Library in Development

This article introduces how to use Alibaba Cloud's open-source yaLanTingLibs C++ compile-time reflection library for tasks like serialization and object manipulation.

yaLanTingLibs is a collection of modern C++ basic tool libraries that are open-sourced by Alibaba Cloud. It includes serialization, HTTP, RPC, coroutine, compile-time reflection, metrics, and log libraries. It can help C++ developers quickly build high-performance C++ applications. At the 2024 Apsara Conference Operating System Technology Workshop, Yu Qi, a senior technical expert at Alibaba Cloud Intelligence Group, purecpp community leader, and author of Deep Application of C++11, shared An In-depth Application of C++20 Compile-time Reflection. This article focuses on the yaLanTingLibs compile-time reflection library and introduces its application scenarios, functions, and features through some cases, as well as its usage in practical development.

The examples in this article recommend installing the Alibaba Cloud Compiler on the Anolis operating system. For installation instructions, please refer to the link.

1. Compile-time Reflection of an Object

First, let's start with a simple example:

#include "ylt/reflection/member_value.hpp"
struct simple {
  int color;
  int id;
  std::string str;
  int age;
};
int main() {
  using namespace ylt::reflection;
  // Obtain the number of simple fields at compile time.
  static_assert(member_count_v<simple> == 4);
  // Obtain the name of the simple type at compile time.
  static_assert(get_struct_name<simple>() == "simple");
  // Obtain the list of simple field names at compile time.
  static_assert(get_member_names<simple>() == std::array<std::string_view, 4>{"color", "id", "str", "age"});
  // Traverse its field names and indexes based on the type.
  for_each<simple>([](std::string_view field_name) {
    std::cout << field_name << "\n";
  }); 
  for_each<simple>([](std::string_view field_name, size_t index) {
    std::cout << index << ", " << field_name << "\n";
  });
}

In addition to APIs for getting these most basic object metadata, ylt::reflection provides more useful APIs:

int main() {
  simple p{.color = 2, .id = 10, .str = "hello reflection", .age = 6};
  // Obtain the field value through the compile-time index.
  CHECK(std::get<0>(p) == 2);
  CHECK(std::get<2>(p) == "hello reflection");
  // Obtain the field name based on the compile-time index.
  static_assert(name_of<simple, 1>()== "id");
  // Obtain the field value based on the compile-time field name.
  CHECK(get<"age"_ylts>(p) == 6);
  CHECK(get<"str"_ylts>(p) == "hello reflection");
  // Obtain the field index based on the compile-time field name.
  static_assert(index_of<simple2, "str"_ylts>() == 2);
  // Traverse the object's fields, field names, and field indexes, and print them.
  for_each(p, [](auto& field, auto name, auto index) {
    std::cout << field << ", " << name << ", " << index << "\n";
  });
  // Access all field values of the object.
  visit_members(p, [](auto&&... args) {
    ((std::cout << args << " "), ...);
    std::cout << "\n";
  });
}

With these compile-time reflection APIs, you can do some interesting things, such as serialization. In theory, an object can be serialized to any format using compile-time reflection. Similarly, yaLanTingLibs also need to use a unified compile-time reflection API to convert objects into formats such as JSON, XML, YAML, Protobuf, and others.

1

2. Serialization based on Compile-time Reflection

Based on the yaLanTingLibs reflection library, it is possible to non-intrusively serialize an object into various data formats, and it can also be extended to support custom formats.

#include "ylt/struct_json/json_reader.h"
#include "ylt/struct_json/json_writer.h"
#include "ylt/struct_xml/xml_reader.h"
#include "ylt/struct_xml/xml_writer.h"
#include "ylt/struct_yaml/yaml_reader.h"
#include "ylt/struct_yaml/yaml_writer.h"
#include "ylt/struct_pb.hpp"
struct simple {
  int color;
  int id;
  std::string str;
  int age;
};
int main() {
  simple p{.color = 2, .id = 10, .str = "hello reflection", .age = 6};
  std::string json;
  struct_json::to_json(p, json);
  std::string xml;
  struct_xml::to_xml(p, xml);
  std::string yaml;
  struct_yaml::to_yaml(p, yaml);
  std::string protobuf;
  struct_pb::to_pb(p, protobuf);
  simple p1;
  struct_json::from_json(p1, json);
  struct_xml::from_xml(p1, xml);
  struct_yaml::from_yaml(p1, xml);
  struct_pb::from_pb(p1, xml);
}

3. Custom Format Serialization through the yaLanTingLibs Reflection Library

The yaLanTingLibs reflection library can be used to easily serialize an object into a custom format. For example, you can serialize an object into a simple JSON5 format, where the key is unquoted.

You only need to use the reflection API for_each to get the object's field values and field names.

struct point {
  int x;
  int y;
};
void test_json5() {
  point pt{2, 4};
  std::string json5;
  json5.append("{");
  ylt::reflection::for_each(pt, [&](auto& field, auto name) {
    json5.append(name).append(":").append(std::to_string(field)).append(",");
  });
  json5.back() = '}';
  CHECK(json5 == "{x:2,y:4}");
}

4. Support for Earlier Version Compilers

The preceding example can be compiled and run only in C++20 compilers (clang13 +, gcc11 +, and msvc2022). If the compiler version is earlier and only supports C++17, can the yaLanTingLibs reflection library be used?

The answer is also yes. The yaLanTingLibs reflection library is compatible with C++17, supporting the earliest version of gcc9. An additional macro definition is required to use the reflection API (which is not needed in higher versions of C++20 compilers). For example, in the previous example:

struct simple {
  int color;
  int id;
  std::string str;
  int age;
};
YLT_REFL(simple, color, id, str, age);
int main() {
  using namespace ylt::reflection;
  // Obtain the number of simple fields at compile time.
  static_assert(member_count_v<simple> == 4);
  // Obtain the name of the simple type at compile time.
  static_assert(get_struct_name<simple>() == "simple");
  // Obtain the list of simple field names at compile time.
  static_assert(get_member_names<simple>() == std::array<std::string_view, 4>{"color", "id", "str", "age"});
  // Traverse its field names and indexes based on the type.
  for_each<simple>([](std::string_view field_name, size_t index) {
    std::cout << index << ", " << field_name << "\n";
  });
}

After the YLT_REFL macro is defined, the reflection API can be used. If the object's fields are private, the macro needs to be defined within the object itself:

struct simple {
YLT_REFL(simple, color, id, str, age);
private:
  int color;
  int id;
  std::string str;
  int age;
};

This approach allows for reflection of private fields but is invasive. If the object has public methods to access private fields, a non-invasive approach can also be implemented:

struct dummy_t5 {
 private:
  int id = 42;
  std::string name = "tom";
  int age = 20;
 public:
  int& get_id() { return id; }
  std::string& get_name() { return name; }
  int& get_age() { return age; }
  const int& get_id() const { return id; }
  const std::string& get_name() const { return name; }
  const int& get_age() const { return age; }
};
YLT_REFL(dummy_t5, get_id(), get_name(), get_age());

If all the object's fields are private and there are no public access methods provided, is reflection still possible? It is possible to reflect private fields without providing public access methods. By using another macro from the yaLanTingLibs reflection library, YLT_REFL_PRIVATE, you can non-intrusively reflect the private fields of an object:

class private_struct {
  int a;
  int b;
 public:
  private_struct(int x, int y) : a(x), b(y) {}
};
YLT_REFL_PRIVATE(private_struct, a, b);

Now you can reflect the private fields of the object:

private_struct st(2, 4);
  for_each(st, [](auto& field, auto name, auto index){
    std::cout << field << ", " << name << ", " << index << "\n";
  });
  visit_members(st, [](auto&... args) {
    ((std::cout << args << " "), ...);
    std::cout << "\n";
  });

5. Summary

The application scenarios for compile-time reflection are very broad, making it highly suitable for serialization scenarios, ORM (Object-Relational Mapping) scenarios, and non-intrusive access to objects. The yaLanTingLibs reflection library not only supports C++20 but is also compatible with C++17. Whether it's earlier version compilers, later version compilers, or scenarios where objects have private fields, a unified set of easy-to-use APIs can be utilized.

0 1 0
Share on

OpenAnolis

90 posts | 5 followers

You may also like

Comments

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

Get Started for Free Get Started for Free