struct_pack的类型系统
struct_pack的特色是具有一套非侵入式的完备的类型系统。其支持结构体,结构体嵌套,各种基本类型和各种数据结构,同时还支持序列化/反序列化用户自定义的数据结构。下面我们将介绍struct_pack的类型系统。
struct_pack支持的序列化类型
struct_pack支持的类型主要包括:基本类型,约束类型,复合类型和兼容类型。
基本类型
包括:有符号和无符号的定长整数,浮点数,字符,布尔类型和空类型。
下列表格列出了struct_pack支持的所有基本类型:
类型名 | 含义 | 编码 |
---|---|---|
int8_t | 有符号定长8位整数 | 补码 |
int16_t | 有符号定长16位整数 | 补码 |
int32_t | 有符号定长32位整数 | 补码 |
int64_t | 有符号定长64位整数 | 补码 |
int128_t(仅GCC/Clang) | 有符号定长128位整数 | 补码 |
uint8_t | 无符号定长8位整数 | 原码 |
uint16_t | 无符号定长16位整数 | 原码 |
uint32_t | 无符号定长32位整数 | 原码 |
uint64_t | 无符号定长64位整数 | 原码 |
uint128_t(仅GCC/Clang) | 无符号定长128位整数 | 原码 |
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 |
bitset | 定长的bit数组,将8个bit压缩为一个bool存储 | std::bitset |
下面我们列出各类型的详细约束条件, 用户可以根据约束条件来定义自己的数据结构:
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会对其进行压缩。
bitset类型
该类需要具有成员函数:size()
,flip()
,set()
,reset()
,count()
,并且size()
函数是constexpr的。该类型的内存布局必须是平凡的,并且将8个bit压缩为一个字节。
template <typename Type>
concept bitset = requires (Type t){
t.flip();
t.set();
t.reset();
t.count();
} && (Type{}.size()+7)/8 == sizeof(Type);
结构体
struct_pack支持结构体类型。结构体内可以包含最多255个字段,并允许结构体嵌套。结构体中的任何成员都必须是struct_pack的合法类型。
struct_pack的结构体类型可以为:struct/class/std::pair/tuplet::tuple/std::tuple
平凡结构体
假如一个结构体类型是struct/class/std::pair/tuplet::tuple
,且其所有的成员字段都是平凡字段,并且该类型未使用STRUCT_PACK_REFL
宏注册,则该结构体被视为平凡结构体类型。
平凡字段是下面几种类型中的一种:
- 基本类型
- 定长数组类型,且数组的元素类型是平凡结构体
trivial_view<T>
类型,且T是平凡类型- 平凡结构体
平凡结构体的特点是,它的类型信息不但含有各字段的类型信息,还包含了该类型的内存对齐值。
struct_pack认为具有不同对齐的平凡结构体是不同的类型,并可以在反序列化时安全的检查出错误。 例如:
#pragma pack(1)
struct foo {
int a;
double b;
};
#pragma()
struct bar {
int a;
double b;
};
void test() {
foo f{};
auto buffer = struct_pack::serialize(f);
auto result = struct_pack::deserialize<bar>(buffer);
// foo和 bar是不同的类型,因为其内存对齐不同,导致其内存布局也不同。
assert(result.has_value() == false);
}
需要注意的是,通过STRUCT_PACK_REFL
宏注册的类型一定不是平凡结构体。 例如:
struct foo {
int a,b,c;
};
struct bar {
int a,b,c;
};
STRUCT_PACK_REFL(bar,a,b,c);
static_assert(struct_pack::get_type_code<foo>()!=struct_pack::get_type_code<bar>());
此外,std::tuple也一定不是平凡结构体。
兼容类型
指的是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具有高效且安全的类型校验能力。