struct_pack的类型系统
struct_pack的特色是具有一套非侵入式的完备的类型系统。其支持结构体,结构体嵌套,各种基本类型和各种数据结构,同时还支持序列化/反序列化用户自定义的数据结构。下面我们将介绍struct_pack的类型系统。
struct_pack支持的序列化类型
struct_pack支持的类型主要包括:基本类型,约束类型,复合类型和兼容类型。
基本类型
包括:有符号和无符号的定长整数,浮点数,字符,布尔类型和空类型。
下列表格列出了struct_pack支持的所有基本类型:
| 类型名 | 含义 | 编码 |
|---|---|---|
| int8_t | 有符号定长8位整数 | 补码 |
| int16_t | 有符号定长16位整数 | 补码 |
| int32_t | 有符号定长32位整数 | 补码 |
| int64_t | 有符号定长64位整数 | 补码 |
| uint8_t | 无符号定长8位整数 | 原码 |
| uint16_t | 无符号定长16位整数 | 原码 |
| uint32_t | 无符号定长32位整数 | 原码 |
| uint64_t | 无符号定长64位整数 | 原码 |
| struct_pack::var_uint32_t | 无符号变长32位整数 | varint变长编码 |
| struct_pack::var_uint64_t | 无符号变长64位整数 | varint变长编码 |
| struct_pack::var_int32_t | 有符号变长32位整数 | varint+zigzag变长编码 |
| struct_pack::var_int64_t | 有符号变长64位整数 | varint+zigzag变长编码 |
| float | 定长32位浮点数 | IEEE-754 单精度编码 |
| double | 定长64位整数 | IEEE-754 双精度编码 |
| char8_t | 8位字符 | 原码 |
| char16_t | 16位字符 | 原码 |
| char32_t | 32位字符 | 原码 |
| wchar_t | wchar字符(长度取决于平台) | 原码 |
| bool | 布尔类型 | 原码 |
| enum/enum class | 枚举类型 | 原码 |
| std::monostate | 空类型 | N/A |
约束类型
约束类型指:满足了一定约束条件的特定数据结构。只要满足约束条件,无论该数据结构是标准库自带的类,还是第三方库提供的类,都属于相同的约束类型
struct_pack支持以下约束类型:
| 类型名 | 含义 | 例如 |
|---|---|---|
| container | 顺序容器类型,存储了若干个元素 | std::vector, std::list, std::deque, folly::flat_vector |
| set_container | 集合类型,存储了若干个键 | std::set, std::unordered_set, std::multiset, boost::container::flat_set, |
| map_container | 映射类型,存储了若干键值对 | std::map, std::unordered_map, std::multimap, boost::container::flat_map |
| string | 字符串类型 | std::string, std::string_view, folly::string, boost::container::string, std::wstring, std::u8string, std::u16string, std::u32string |
| array | 数组类型,其长度编译期确定 | C语言内置数组类型,std::array |
| optional | optional类型 | std::optional, boost::optional |
| variant | variant类型 | std::variant |
| expected | expected类型,包含期望结果或错误码 | std::expected, tl::expected |
| unique_ptr | unique_ptr类型,一个独占所有权的指针 | std::unique_ptr |
下面我们列出各类型的详细约束条件, 用户可以根据约束条件来定义自己的数据结构:
container类型
该类需要提供:value_type类型成员和size(),begin(),end()成员函数,同时不满足set_container,map_container和string的约束条件。value_type必须也是合法的struct_pack类型。
template <typename Type>
concept container = requires(Type container) {
typename std::remove_cvref_t<Type>::value_type;
container.size();
container.begin();
container.end();
} && !set_container && !map_container && string;如果该类型的内存布局是连续的,struct_pack会启用memcpy优化。
set_container类型
该类需要提供:value_type类型成员和key_type类型成员,同时提供size(),begin(),end()成员函数。value_type必须也是合法的struct_pack类型。
template <typename Type>
concept set_ontainer = requires(Type container) {
typename std::remove_cvref_t<Type>::value_type;
typename std::remove_cvref_t<Type>::key_type;
container.size();
container.begin();
container.end();
};map_container类型
该类需要提供:value_type类型成员和mapped_type类型成员,同时提供size(),begin(),end()成员函数。value_type和mapped_type必须也是合法的struct_pack类型。
template <typename Type>
concept map_container = requires(Type container) {
typename std::remove_cvref_t<Type>::value_type;
typename std::remove_cvref_t<Type>::mapped_type;
container.size();
container.begin();
container.end();
};string类型
该类需要提供:value_type类型成员,且value_type是字符类型,同时提供size(),begin(),end(),length(),data()成员函数。value_type必须也是合法的struct_pack类型。
template <typename Type>
concept is_char_t = std::is_same_v<Type, signed char> ||
std::is_same_v<Type, char> || std::is_same_v<Type, unsigned char> ||
std::is_same_v<Type, wchar_t> || std::is_same_v<Type, char16_t> ||
std::is_same_v<Type, char32_t> || std::is_same_v<Type, char8_t>;
template <typename Type>
concept string = requires(Type container) {
requires is_char_t<typename std::remove_cvref_t<Type>::value_type>;
container.size();
container.begin();
container.end();
container.length();
container.data();
};如果该类型的内存布局是连续的,struct_pack会启用memcpy优化。 如果序列化到string_view类型,struct_pack会启用零拷贝优化。
array类型
该类型需要满足:是C语言内置数组类型,或者该类型具有size()成员函数且特化了std::tuple_size。数组的成员必须也是合法的struct_pack类型。
template <typename Type>
concept array = std::is_array_v<T> || requires(Type arr) {
arr.size();
std::tuple_size<std::remove_cvref_t<Type>>{};
};如果该类型的内存布局是连续的,struct_pack会启用memcpy优化。
variant类型
该类型只能为std::variant类型,是一个类型安全的union。variant包含的类型必须也是合法的struct_pack类型。
expected类型
该类需要提供:value_type, error_type和unexpected_type类型成员,同时提供has_value(),error(),value()成员函数。value_type和error_type必须也是合法的struct_pack类型。
template <typename Type>
concept expected = requires(Type e) {
typename std::remove_cvref_t<Type>::value_type;
typename std::remove_cvref_t<Type>::error_type;
typename std::remove_cvref_t<Type>::unexpected_type;
e.has_value();
e.error();
requires std::is_same_v<void,
typename std::remove_cvref_t<Type>::value_type> ||
requires(Type e) {
e.value();
};
};该类型序列化的大小取决于value_type, error_type的最大长度。
optional类型
该类需要提供:value_type类型成员,同时提供has_value(),、value()成员函数,重载了operator *,并且不满足expected类型的约束。value_type必须也是合法的struct_pack类型。
template <typename Type>
concept optional = !expected<Type> && requires(Type optional) {
optional.value();
optional.has_value();
optional.operator*();
typename std::remove_cvref_t<Type>::value_type;
};如果该类型为空,struct_pack会对其进行压缩。
unique_ptr类型
该类需要提供:operator*,且禁用了拷贝赋值,同时定义了element_type类型成员。
template <typename Type>
concept unique_ptr = requires(Type ptr) {
ptr.operator*();
typename std::remove_cvref_t<Type>::element_type;
}
&&!requires(Type ptr, Type ptr2) { ptr = ptr2; };如果该对象的值为空指针,struct_pack会对其进行压缩。
结构体
struct_pack支持结构体类型。结构体内可以包含最多64个字段,并允许结构体嵌套。结构体中的任何成员都必须是struct_pack的
struct_pack将结构体分为两类:
平凡结构体
包括C++语言中原生的struct/class, std::pair和tuplet::tuple。
例如:
struct person {
int age;
std::string name;
};和
std::pair<int,std::string>以及
tuplet::tuple<int,std::string>在 struct_pack中被视作相同的类型。
非平凡结构体
指std::tuple,由于历史原因,其内存布局和普通的struct不同,出于优化考虑,我们在类型系统中将其和平凡结构体区分开来。
例如:
struct person {
int age;
std::string name;
};和
std::tuple<int,std::string>这两者是不同的类型,前者是平凡的结构体,而后者是非平凡的。
兼容类型
指的是struct_pack::compatible<T, version_number>类型,这是一种特殊的类型,从C++语言的角度上看,它和std::optional类型类似,但是它在struct_pack语言中的语义十分特殊:表示添加一个可向前/向后兼容的类型。
struct_pack保证对象中新增的compatible<T, version_number>字段,序列化后的二进制数据可以被安全的反序列化为旧版本的对象,多余的数据会被舍弃。
struct_pack同样保证旧版本的对象,序列化后的二进制数据可以被安全的反序列化为添加了compatible<T, version_number>字段的新版本对象。这些新字段反序列化的结果将是空值(std::nullopt)。
默认的version_number为0。
例如,我们定义一个新版本的person:
struct person_v2 {
int age;
std::string name;
struct_pack::compatible<std::string> nick_name;
};我们可以保证其向前兼容性:
auto buffer = struct_pack::serialize(person_v2{.age=24,.name="Betty",.nick_name="NULL"});
auto res = struct_pack::deserialize<person>(buffer);
assert(res.has_value()==true);
assert(res->age==24);
assert(res->name=="Betty");也可以保证其向后兼容性
auto buffer = struct_pack::serialize(person{.age=24,.name="Betty"});
auto res = struct_pack::deserialize<person_v2>(buffer);
assert(res.has_value()==true);
assert(res->age==24);
assert(res->name=="Betty");
assert(res->nick_name.has_value()==false);注意:struct_pack::compatible<T>中包含的类型T必须是一个合法的struct_pack类型。
注意,在修改字段的过程中,如果想保证兼容性,必须遵循只增不改原则:
- 只增:新添加的字段必须是
struct_pack::compatible<T, version_number>类型,且version_number必须大于上一次填入的版本号。 - 不该:不修改/删除任何一个旧的字段。
如果删去/修改了旧的struct_pack::compatible<T, version_number>类型,或者新增的struct_pack::compatible<T, version_number>类型版本号不是递增的,则两个结构体不能相互兼容,并且在两个不同版本之间序列化/反序列化的行为是未定义的!
struct_pack的类型信息和类型哈希
类型哈希校验
struct_pack的类型校验依赖于编译期获取的类型信息,并在编译期执行哈希计算,从而得到用于校验的类型哈希码。
struct_pack计算类型哈希的步骤如下:
- 通过静态反射,从待序列化的类型生成一棵struct_pack类型树。
- 在编译期递归遍历这颗树,生成与类型树对应的类型字符串。
- 在编译期计算字符串的32位MD5哈希值。
如下图所示:

对于结构体
struct dummy {
int32_t id;
double number;
std::string str;
};其最外层是一个结构体,结构体作为类型树的根节点,包含了三个子成员:id,number和str。前两者是基本类型,因此是类型树的叶子节点。后者是string约束类型,其包含的子类型是char,因此有一个char类型的子节点。
每个不同的类型都有独一无二的类型id。生成对应的类型树后,我们可以在编译期遍历这棵树,然后根据类型id得到唯一映射了类型树的字符串,最后在编译期计算该字符串的32位MD5哈希值,即可得到该类型的哈希校验码。
该校验码将会被保存到struct_pack序列化数据的头部,在反序列化时,我们会首先读取数据中的校验码是否和待反序列化的类型相同。由于类型计算在编译期完成,因此无论其有多么复杂,运行时都只需要简单的对一个32位的整数进行比较即可,这无疑是非常高效的。
一旦检查到哈希校验码不符合预期,struct_pack都会返回一个错误码:struct_pack::errc::invalid_argument。
兼容性问题
struct_pack允许用户给旧版本的结构体添加struct_pack::compatible<T>字段。为了保证兼容性,我们的类型校验必须跳过这一字段。具体的来说,我们在生成类型字符串的过程中会略去类型树中根节点为struct_compatible<T>类型的子树。这样就保证其哈希码和旧版本的结构体相同。
哈希冲突
struct_pack使用的32位MD5哈希值(由于最低位被struct_pack用于存储其他信息,有效位数只有31位),完全可能被构造出哈希冲突。反序列化时,如果发生哈希冲突,struct_pack的行为是未定义的。但是这一概率很低,不考虑恶意构造的情况下,随机写一个错误的类型,反序列化到该类型且发生哈希冲突的概率是2的负-31次方。考虑到写错类型的概率本来就很低,因此哈希冲突应该是很罕见的行为。
为了缓解哈希冲突,struct_pack默认情况下会在debug模式中给序列化数据添加完整的类型字符串并进行完整的校验。因此,只要用户在debug模式中测试过自己的代码,即使不小心写错了类型,并且极其倒霉的发生了哈希冲突,也能在debug模式下被检测出来。
综上,struct_pack具有高效且安全的类型校验能力。