Featured image of post 现代反射实现(三):Json序列化

现代反射实现(三):Json序列化

利用编译期反射实现 Json 序列化

第三章:基于反射的 Json 序列化

代码实现方案参考:https://github.com/qicosmos/iguana


通过前面的努力,我们目前已经有了以下能力:

  • 通过编译器内置宏拿到成员名
  • 通过结构化绑定和打表拿到成员引用 tuple
  • 通过递归构造拿到成员数量

简单说,现在我们已经能在编译期拿到一个结构体的“成员列表”,并在运行时获得对应的引用。

有了这些能力,我们就能实现:一行代码序列化结构体对象,摒弃繁琐的Json拼装过程:

1
2
3
struct Complex obj;
std::string strJson;
tinyrefl::reflection_to_json(obj, strJson);     // 序列化

最终支持的能力包括:

1
2
3
4
5
6
7
// 普通字段
int / double / bool / std::string / char* 
// 嵌套 struct
std::vector / std::list / std::deque
// 容器和多层嵌套容器
std::map<std::string, T> / std::unordered_map<std::string, T>
vector<vector<int>> / vector<vector<T>> 

实现方案

目前传统的Json支持各种类型,如:对象、数组、字符串、数字、Bool等。

类型不同,Json的输出方式也不同,比如字符串类型可能需要双引号进行包裹,数组类型或对象类型则需要括号来标识,想要将结构体序列化成一个Json格式的字符串,就需要对结构体中的各种类型分别进行处理。

做编译期类型区分,模板显然是再适合不过了。

前两章已经把“怎么拿到成员”这件事解决掉了:我们能拿到 members_count_v<T>,能拿到成员名数组,也能遍历到每个成员的引用。序列化阶段真正要解决的问题,其实只有一个:

对于任意一个 member_reference,它到底应该被当成什么类型写进 Json。

典型几种情况:

  • 内置算术类型,按数字写
  • 布尔类型,写成 true / false
  • 字符串、字符指针,加引号并做转义
  • 容器,写成数组或对象
  • 自定义聚合类型,展开成对象并递归处理成员

在类型分发之前,首先我们需要能够遍历结构体成员。

结构体成员循环遍历

对外只暴露一行接口:

1
2
template <detail::AggregateType T, detail::OutputStream Stream>
inline void reflection_to_json(T&& object, Stream &stream);

其中AggregateType是目前序列化所支持的成员类型,OutputStream是我们能够支持的输出类型,可以只限定为std::string

1
2
3
4
5
6
7
8
9
// 检查是否为聚合类型
template <typename T>
concept AggregateType = ::std::is_aggregate_v<remove_cvref_t<T>>;
// 检查字符串类型
template<typename Stream>
concept OutputStream = requires(Stream& s) {{ s.append("abc") };};
// 或
template<typename Stream>
concept OutputStream = ::std::is_same_v<std::remove_cvref_t<Stream>, std::string>;

在第一层类型检查通过后,我们就需要对具体类型进行递归分发了,首先我们要能够循环的遍历结构体成员:

1
2
3

template <typename T, typename Function>
inline void for_each_member(T&& object, Function&& function);

这里留出两个参数,object是结构体成员,function是对不同结构体成员的处理回调

先拿到我们需要的基本信息:成员名称和成员数量

1
2
3
using object_type = remove_cvref_t<T>;
constexpr member_array<object_type> object_name_array = struct_members_to_array<object_type>();
constexpr size_t object_member_count = members_count_v<object_type>;

作为一个反射的结构体成员遍历,我们需要检查回调函数是否能够接收:成员引用、成员名称、当前成员索引,否则抛出错误。

1
2
3
4
5
6
    if constexpr (::std::is_invocable_v<Function, decltype(struct_member_reference<0>(object)), ::std::string_view, size_t>) {
        ...
    } else {
        static_assert(::std::is_invocable_v<Function, ::std::string_view, size_t>, "invalid function args,  \
            param is: [::std::string_view, size_t]");
    }

检查后调用回调:

1
2
3
[&]<size_t... Is>(::std::index_sequence<Is...>) {
    (function(struct_member_reference<Is>(object), object_name_array[Is], Is), ...);
}(::std::make_index_sequence<object_member_count>{});

这是一个模板Lambda表达式,在C++20的加持下,很多代码都可以写的非常简单,这里展开其实是写了很多的回调函数,依次传入不同的索引:

1
2
3
4
(function(struct_member_reference<0>(object), object_name_array[0], 0), ...);
(function(struct_member_reference<1>(object), object_name_array[1], 1), ...);
(function(struct_member_reference<2>(object), object_name_array[2], 2), ...);
...

因为成员类型是在回调函数被调用时才能确定的,所以这里需要一个模板函数,我们可以用auto来代指模板:

1
2
3
[&](auto&& member_reference, auto&& member_name, auto&& member_index) {
    ...
}

效果和:

1
2
 template<typename MemberRef, typename Name, typename Index>
    void CallbackFunc(MemberRef&& member_reference, Name&& member_name, Index&& member_index) const;

是一样的,不过模板函数在未实例化之前是没有确切地址的,所以不能作为参数传入,所以需要一层封装:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
struct Callback {
    Stream& stream;
    template<typename MemberRef, typename Name, typename Index>
    void operator()(MemberRef&& member_reference,
                    Name&& member_name, Index member_index) const {
        ...
    }
};

Callback cb{ stream };
detail::for_each_member(std::forward<T>(object), cb);

分发不同Json类型

类型区分

这里就要用到我们前面介绍的模板约束了,对于不同的类型,在编译期匹配不同的模板:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// Check is sequecnce container
template <typename T>
inline constexpr bool is_sequence_container_v =
    is_template_instant_of<::std::deque, remove_cvref_t<T>>::value  ||
    is_template_instant_of<::std::list, remove_cvref_t<T>>::value   ||
    is_template_instant_of<::std::vector, remove_cvref_t<T>>::value;

// Check is char*
template <typename T>

inline constexpr bool is_char_pointer_v = is_char_v<::std::remove_pointer_t<::std::remove_cvref_t<T>>> &&
                        ::std::is_pointer_v<::std::remove_cvref_t<T>>;

// Check is char array
template <typename T>
inline constexpr bool is_char_array_v = ::std::is_array_v<remove_cvref_t<T>> && is_char_v<::std::remove_extent_t<T>>;

// Check is bool
template <typename T>
inline constexpr bool is_bool_v = ::std::is_same_v<remove_cvref_t<T>, bool>;

// Check is int
template <typename T>
inline constexpr bool is_int_v = ::std::is_integral_v<remove_cvref_t<T>> && 
                        !is_char_v<T> &&
                        !is_char_pointer_v<T> &&
                        !is_char_array_v<T> &&
                        !is_bool_v<T>;
...

可以看到我在int类型的检查处做了很多’非’的判断,是因为假设我同时处理int类型和char类型,对于约束来说:

1
2
is_char_v<char> == true
std::is_integral_v<char> == true

也就是说,这两个模板的 requires 对 char 都成立,编译器会认为两个重载都“可行”,从而触发编译错误,有多个匹配的函数,同理,对于intbool也是同理。

对于用户自定义的结构体类型,只需要把目前支持的类型全都false掉就好。

函数实现

对于普通类型:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// int
template <OutputStream Stream, typename T>
requires (is_int_v<T> || is_int64_v<T> || is_float_v<T> || is_double_v<T>)
inline void to_json_value(Stream&& s, T&& object) {
    s.append(::std::to_string(object));
}
// string to json
template <OutputStream Stream, typename T>
inline void to_json_value(Stream&& s, T&& object) requires is_string_v<T> {
    s.append("\"");
    s.append(object.data(), object.size());
    s.append("\"");
}
// char to json
template <OutputStream Stream, typename T>
inline void to_json_value(Stream&& s, T&& object) requires is_char_v<T> {
    s.append("\"");
    s.push_back(object);
    s.append("\"");
}
// bool
template <OutputStream Stream, typename T>
inline void to_json_value(Stream&& s, T&& object) requires is_bool_v<T> {
    s.append(object ? "true" : "false");
}
...

对于顺序容器和关联容器,需要遍历其容器内数据,这时候迭代器的好处就体现出来了,我们可以统一的用迭代器处理各种容器,只需要写一套代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// sequence to json
template <OutputStream Stream, typename T>
inline void to_json_value(Stream&& s, T&& object) requires is_sequence_container_v<T> {
    s.append("[");
    for_each_by_iterator(s, object.cbegin(), object.cend(), ",", [&](const auto& member) {
        to_json_value(s, member);
    });
    s.append("]");
}
// associative to json
template <OutputStream Stream, typename T>
inline void to_json_value(Stream&& s, T&& object) requires is_associative_container_v<T> {
    s.append("{");
    for_each_by_iterator(s, object.cbegin(), object.cend(), ",", [&](const auto& pair_value) {  // ::std::pair
        if constexpr (is_string_v<decltype(pair_value.first)>) {
            to_json_key(s, pair_value.first);
            s.append(":");
            to_json_value(s, pair_value.second);
        } else {
            static_assert(is_string_v<decltype(pair_value.first)>, "Only string keys are supported in JSON");
        }
    });
    s.append("}");
}

其中for_each_by_iterator和前面介绍的结构体遍历实现类似,这里就不过多介绍了。 特别的,对于关联容器,需要在回调检查Key类型为std::string类型,这才符合Json的KV格式。

对于自定义struct,只需要调用最开始实现的序列化函数就能处理:

1
2
3
4
template <OutputStream Stream, typename T>
inline void to_json_value(Stream&& s, T&& object) requires is_custom_type_v<T> {
    ::tinyrefl::reflection_to_json(object, s);
}

至此,整个序列化功能实现闭环。


本章总结

本章的代码见: https://github.com/SSmallOrange/TinyReflection/blob/master/tinyrefl/reflectiopn_to_json.hpp

本章相关测试代码: https://github.com/SSmallOrange/TinyReflection/blob/master/test/test_reflection_to_json.cpp

有了序列化能力自然也需要一个反序列化能力,下一章实现。

Licensed under CC BY-NC-SA 4.0
comments powered by Disqus

本博客已稳定运行 小时 分钟
共发表 7 篇文章 · 总计 27.20 k 字
本站总访问量