diff --git a/README.md b/README.md index 59774cf1..f2df9c2e 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,10 @@ This library provides a portable cross-platform way of: - serialization of yaml - serialization of any customized format +### compile time reflection ### + +[reflection lib introduction](lang/reflection_introduction.md) + ### Tutorial ### This Tutorial is provided to give you a view of how *iguana* works for serialization. @@ -38,10 +42,12 @@ struct person std::string name; int age; }; -REFLECTION(person, name, age) //define meta data +#if __cplusplus >= 202002L +YLT_REFL(person, name, age) //define meta data +#endif ``` -Defining meta data is very simple, and just needs to define in a `REFLECTION` macro. +Defining meta data is very simple, if your compiler is C++20 compiler(gcc11+, clang13+, msvc2022), no need define YLT_REFL, other wise need to define in a `YLT_REFL` macro. Now let's serialize `person` to `json` string. @@ -141,7 +147,7 @@ struct one_t { int id; }; -REFLECTION(one_t, id); +YLT_REFL(one_t, id); struct two { @@ -149,7 +155,7 @@ struct two one_t one; int age; }; -REFLECTION(two, name, one, age); +YLT_REFL(two, name, one, age); struct composit_t { @@ -161,7 +167,7 @@ struct composit_t double f; std::list g; }; -REFLECTION(composit_t, a, b, c, d, e, f, g); +YLT_REFL(composit_t, a, b, c, d, e, f, g); ``` Then call the simple interface: @@ -187,13 +193,13 @@ struct book_t { std::string author; float price; }; -REFLECTION(book_t, author, price); +YLT_REFL(book_t, author, price); struct library_t { std::string name; int id; std::vector book; }; -REFLECTION(library_t, name, id, book); +YLT_REFL(library_t, name, id, book); ``` And then, simply call the interface: @@ -236,7 +242,7 @@ struct plain_type_t { std::optional num; std::optional price; }; -REFLECTION(plain_type_t, isok, status, c, hasprice, num, price); +YLT_REFL(plain_type_t, isok, status, c, hasprice, num, price); ``` And then, simply call the interface: @@ -311,7 +317,7 @@ struct enum_t { Status a; Status b; }; -REFLECTION(enum_t, a, b); +YLT_REFL(enum_t, a, b); // deserialization enum_t e; @@ -333,7 +339,7 @@ iguana::to_json(e1, ss); ### Scripts -Automatically generate `REFLECTION` macros based by struct. +Automatically generate `YLT_REFL` macros based by struct. To get a list of basic options and switches use: @@ -382,11 +388,11 @@ struct person { std::string name; int age; }; -REFLECTION(person, name, age); +YLT_REFL(person, name, age); char *iguana = NULL; struct composit_t { int a; std::vector b; int c; std::map d; std::unordered_map e; double f;}; -REFLECTION(composit_t, a, b, c, d, e, f); +YLT_REFL(composit_t, a, b, c, d, e, f); struct composit_t2 { @@ -397,7 +403,7 @@ struct composit_t2 std::unordered_map random_name__; double __f__number__complex; }; -REFLECTION(composit_t2, a, b, iguana, example_test, random_name__, __f__number__complex); +YLT_REFL(composit_t2, a, b, iguana, example_test, random_name__, __f__number__complex); ``` other example: @@ -406,7 +412,7 @@ other example: python automatic_macro_generator.py -i test_macro_generator.cpp -o have_macro.cpp ``` -test_macro_generator.cpp will be unchanged, have_macro.cpp will be changed to source file with REFLECTION macro. +test_macro_generator.cpp will be unchanged, have_macro.cpp will be changed to source file with YLT_REFL macro. scripts works out of the box with Python version 2.7 and 3.x on any platform. @@ -417,7 +423,7 @@ Notes: In Python3,Will prompt `DeprecationWarning: 'U' mode is deprecated`.Ignor - **Question**: Why is the library called *iguana*? - - **Answer**: I think serialization is like an iguana, because the only difference is the displaying format, however the meta data is never changed. With changeless meta data and reflection, you can serialize an object to any format, which is like how an iguana does. + - **Answer**: I think serialization is like an iguana, because the only difference is the displaying format, however the meta data is never changed. With changeless meta data and YLT_REFL, you can serialize an object to any format, which is like how an iguana does. - **Question**: Does *iguana* support raw pointer? diff --git "a/lang/iguana \344\275\277\347\224\250\346\226\207\346\241\243.md" "b/lang/iguana \344\275\277\347\224\250\346\226\207\346\241\243.md" index 8e66cda4..d8aa4dfc 100644 --- "a/lang/iguana \344\275\277\347\224\250\346\226\207\346\241\243.md" +++ "b/lang/iguana \344\275\277\347\224\250\346\226\207\346\241\243.md" @@ -19,7 +19,7 @@ iguana 是header only的,从github 将iguana clone 下来之后,在cmake 里 gcc9+、clang11+、msvc2019+ # json 序列化/反序列化 -iguana 序列化需要先定义一个可反射的对象,通过REFLECTION 可以轻松定义一个可反射对象。 +iguana 序列化需要先定义一个可反射的对象,通过YLT_REFL 可以轻松定义一个可反射对象。 ```c++ struct person @@ -27,9 +27,9 @@ struct person std::string_view name; int age; }; -REFLECTION(person, name, age); // 通过这个宏定义元数据让person 成为一个可反射对象 +YLT_REFL(person, name, age); // 通过这个宏定义元数据让person 成为一个可反射对象 ``` -通过REFLECTION 宏定义元数据之后就可以一行代码实现json 的序列化与反序列化了。 +通过YLT_REFL 宏定义元数据之后就可以一行代码实现json 的序列化与反序列化了。 ```c++ person p = { "tom", 28 }; @@ -74,7 +74,7 @@ struct some_obj { std::string_view name; iguana::numeric_str age; }; -REFLECTION(some_obj, name, age); +YLT_REFL(some_obj, name, age); void test_view(){ std::string_view str = "{\"name\":\"tom\", \"age\":20}"; @@ -110,7 +110,7 @@ struct some_obj { std::string_view name; int age; }; -REFLECTION(some_obj, name, age); +YLT_REFL(some_obj, name, age); void test() { person p = {"admin", 20}; @@ -135,7 +135,7 @@ struct person { std::string_view name; int age; }; -REFLECTION(person, name, age); +YLT_REFL(person, name, age); void test_pretty() { person p{"tom", 20}; @@ -171,11 +171,11 @@ struct book_t { std::string title; std::string author; }; -REFLECTION(book_t, title, author); +YLT_REFL(book_t, title, author); struct library { iguana::xml_attr_t book; }; -REFLECTION(library, book); +YLT_REFL(library, book); TEST_CASE("test library with attr") { auto validator = [](library lib) { CHECK(lib.book.attr()["id"] == "1234"); @@ -214,7 +214,7 @@ struct some_obj { std::string_view name; int age; }; -REFLECTION(some_obj, name, age); +YLT_REFL(some_obj, name, age); void test() { std::string_view xml = R"( @@ -249,7 +249,13 @@ public: person() = default; person(std::shared_ptr id, std::unique_ptr name) : id_(id), name_(std::move(name)) {} - REFLECTION_ALIAS(person, "rootnode", FLDALIAS(&person::id_, "id"), FLDALIAS(&person::name_, "name")); + static constexpr auto get_alias_field_names(person*) { + return std::array{field_alias_t{"id", 0}, field_alias_t{"name", 1}}; + } + static constexpr std::string_view get_alias_struct_name(person*) { + return "rootnode"; + } + YLT_REFL(person, id_, name_); void parse_xml() { iguana::from_xml(*this, xml_str); @@ -259,18 +265,18 @@ public: } } ``` -REFLECTION_ALIAS 中需要填写结构体的别名和字段的别名,通过别名将标签和结构体字段关联起来就可以保证正确的解析了。 +get_alias_field_names 函数中需要填写结构体的别名,get_alias_struct_name 函数中填写字段的别名,别名字段的索引需要和YLT_REFL宏定义的字段顺序一致,通过别名将标签和结构体字段关联起来就可以保证正确的解析了。 这个例子同时也展示了iguana 支持智能指针的序列化和反序列化。 # 如何处理私有字段 -如果类里面有私有字段,在外面定义REFLECTION 宏会出错,因为无法访问私有字段,这时候把宏定义到类里面即可,但要保证宏是public的。 +如果类里面有私有字段,在外面定义YLT_REFL 宏会出错,因为无法访问私有字段,这时候把宏定义到类里面即可,但要保证宏是public的。 ```c++ class person { std::string name; int age; public: - REFLECTION(person, name, age); + YLT_REFL(person, name, age); }; ``` @@ -289,7 +295,7 @@ struct plain_type_t { std::optional num; std::optional price; }; -REFLECTION(plain_type_t, isok, status, c, hasprice, num, price); +YLT_REFL(plain_type_t, isok, status, c, hasprice, num, price); ``` ```c++ @@ -320,7 +326,7 @@ struct enum_t { Status a; Status b; }; -REFLECTION(enum_t, a, b); +YLT_REFL(enum_t, a, b); namespace iguana { template <> struct enum_value { @@ -390,7 +396,7 @@ struct test_float_t { double a; float b; }; -REFLECTION(test_float_t, a, b); +YLT_REFL(test_float_t, a, b); void user_defined_tochars_example() { test_float_t t{2.011111, 2.54}; std::string ss; diff --git a/lang/reflection_introduction.md b/lang/reflection_introduction.md new file mode 100644 index 00000000..605176c6 --- /dev/null +++ b/lang/reflection_introduction.md @@ -0,0 +1,236 @@ +yalantinglibs 的编译期反射库 + +yalantinglibs的编译期反射库提供了统一的访问对象元数据的API,让编译期反射变得简单,支持的特性也很丰富,使用C++20的时候尤其方便。 + +# 编译期反射一个对象 +从一个简单的例子开始: +```cpp +#include "ylt/reflection/member_value.hpp" + +struct simple { + int color; + int id; + std::string str; + int age; +}; + +int main() { + using namespace ylt::reflection; + + // 编译期获取simple 字段个数 + static_assert(member_count_v == 4); + + // 编译期获取simple 类型的名称 + static_assert(get_struct_name() == "simple"); + + // 编译期获取simple 字段名称列表 + static_assert(get_member_names() == std::array{"color", "id", "str", "age"}); + + // 根据类型遍历其字段名和索引 + for_each([](std::string_view field_name) { + std::cout << field_name << "\n"; + }); + + for_each([](std::string_view field_name, size_t index) { + std::cout << index << ", " << field_name << "\n"; + }); +} +``` + +除了获取这些最基本的对象元数据的API之外,ylt::reflection还提供了更多有用的API: + +```cpp +int main() { + simple p{.color = 2, .id = 10, .str = "hello reflection", .age = 6}; + + // 通过编译期索引获取字段值 + CHECK(std::get<0>(p) == 2); + CHECK(std::get<2>(p) == "hello reflection"); + + // 根据编译期索引获取字段名 + static_assert(name_of()== "id"); + + // 根据编译期字段名获取字段值 + CHECK(get<"age"_ylts>(p) == 6); + CHECK(get<"str"_ylts>(p) == "hello reflection"); + + // 根据编译期字段名获取字段索引 + static_assert(index_of() == 2); + + // 遍历对象的字段、字段名、字段索引, 并打印 + for_each(p, [](auto& field, auto name, auto index) { + std::cout << field << ", " << name << ", " << index << "\n"; + }); + + // 访问对象的所有字段值 + visit_members(p, [](auto&&... args) { + ((std::cout << args << " "), ...); + std::cout << "\n"; + }); +} +``` + +有了这些编译期反射的API之后就可以做一些有趣的事了,比如做序列化,理论上可以通过编译期反射将一个对象序列化到任意格式。事实上yalantinglibs 也是这样做的,通过统一的编译期反射API将对象转换为json,xml,yaml,protobuf等格式。 + +![img](reflection.png) + +# 基于编译期反射的序列化 +基于yalantinglibs 反射库可以非侵入式的将一个对象序列化到多种数据格式,也可以扩展支持自定义的格式。 + +```cpp +#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); +} +``` + +## 使用yalantinglibs reflection 库实现自定义格式的序列化 +使用yalantinglibs reflection 库可以很方便的将对象序列化到自定义格式,比如将一个对象序列化成一个简单的json5 格式,json5的key是没有引号的。 + +只需要通过反射API for_each 拿到对象的字段值和字段名就够了。 + +```cpp +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}"); +} +``` + +# 低版本编译器的支持 +前面的example 只能在C++20 高版本编译器(clang13+, gcc11+, msvc2022)中才能编译运行。 + +如果编译器版本较低只支持C++17 能用yalantinglibs 的反射库吗? + +也是可以的,yalantinglibs 的反射库兼容了C++17,支持的最低版本是gcc9,不过需要宏定义一个额外的宏才能使用反射API(C++20 高版本编译器中是不需宏的),比如之前的例子: + +```cpp +struct simple { + int color; + int id; + std::string str; + int age; +}; +YLT_REFL(simple, color, id, str, age); + +int main() { + using namespace ylt::reflection; + + // 编译期获取simple 字段个数 + static_assert(member_count_v == 4); + + // 编译期获取simple 类型的名称 + static_assert(get_struct_name() == "simple"); + + // 编译期获取simple 字段名称列表 + static_assert(get_member_names() == std::array{"color", "id", "str", "age"}); + + // 根据类型遍历其字段名和索引 + for_each([](std::string_view field_name, size_t index) { + std::cout << index << ", " << field_name << "\n"; + }); +} +``` +定义了YLT_REFL 宏之后就可以使用反射的API了,如果对象的字段都是私有的,则需要将宏定义到对象内部: +```cpp +struct simple { +YLT_REFL(simple, color, id, str, age); +private: + int color; + int id; + std::string str; + int age; +}; +``` +这种方式可以反射私有字段但有侵入性,如果对象有public的访问私有字段的方法也可做到非侵入式: + +```cpp +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()); +``` + +如果对象字段都是私有的,并且没有提供public的访问方法还能反射吗?也是可以的,可以通过yalantinglibs reflection另外一个宏YLT_REFL_PRIVATE非侵入式的反射对象的私有字段: +```cpp +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); +``` + +现在就可以反射对象的私有字段了: + +```cpp + 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"; + }); +``` + +# 总结 +编译期反射的应用场景很广泛,非常适合用在序列化场景、ORM(实体-映射)场景、非侵入式访问对象等场景,yalantinglibs 的反射库在支持C++20的同时也兼容了C++17,不论是低版本编译器还是高版本编译器,或者对象存在私有字段等场景,都能使用统一的一套易用的API。 diff --git a/lang/struct_pb_intro.md b/lang/struct_pb_intro.md index a8aa637d..ee6c5257 100644 --- a/lang/struct_pb_intro.md +++ b/lang/struct_pb_intro.md @@ -16,14 +16,14 @@ struct my_struct { bool y; struct_pb::fixed64_t z; }; -REFLECTION(my_struct, x, y, z); +YLT_REFL(my_struct, x, y, z); struct nest { std::string name; my_struct value; int var; }; -REFLECTION(nest, name, value, var); +YLT_REFL(nest, name, value, var); ``` ### 序列化 @@ -66,7 +66,7 @@ struct my_struct { bool y; iguana::fixed64_t z; }; -REFLECTION(my_struct, x, y, z); +YLT_REFL(my_struct, x, y, z); struct nest1 : public iguana::base_impl { nest1() = default; @@ -76,7 +76,7 @@ struct nest1 : public iguana::base_impl { my_struct value; int var; }; -REFLECTION(nest1, name, value, var); +YLT_REFL(nest1, name, value, var); ``` ```cpp diff --git a/scripts/automatic_macro_generator.py b/scripts/automatic_macro_generator.py index c7e194c3..a762a718 100644 --- a/scripts/automatic_macro_generator.py +++ b/scripts/automatic_macro_generator.py @@ -85,8 +85,8 @@ def main(): # get str member_list = get_members_in_list(content) content = "" - # get reflection macro string - reflection_str = "REFLECTION(" + struct_name + ',' + # get YLT_REFL macro string + reflection_str = "YLT_REFL(" + struct_name + ',' length_of_members = len(member_list) count = 0 for member in member_list: