Skip to content
On this page

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_t8位字符原码
char16_t16位字符原码
char32_t32位字符原码
wchar_twchar字符(长度取决于平台)原码
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
optionaloptional类型std::optional, boost::optional
variantvariant类型std::variant
expectedexpected类型,包含期望结果或错误码std::expected, tl::expected
unique_ptrunique_ptr类型,一个独占所有权的指针std::unique_ptr
bitset定长的bit数组,将8个bit压缩为一个bool存储std::bitset

下面我们列出各类型的详细约束条件, 用户可以根据约束条件来定义自己的数据结构:

container类型

该类需要提供:value_type类型成员和size(),begin(),end()成员函数,同时不满足set_container,map_containerstring的约束条件。value_type必须也是合法的struct_pack类型。

cpp
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类型。

cpp
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_typemapped_type必须也是合法的struct_pack类型。

cpp
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类型。

cpp
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类型。

cpp
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_typeunexpected_type类型成员,同时提供has_value(),error(),value()成员函数。value_typeerror_type必须也是合法的struct_pack类型。

cpp
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类型。

cpp
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类型成员。

cpp
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压缩为一个字节。

cpp
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宏注册,则该结构体被视为平凡结构体类型。

平凡字段是下面几种类型中的一种:

  1. 基本类型
  2. 定长数组类型,且数组的元素类型是平凡结构体
  3. trivial_view<T> 类型,且T是平凡类型
  4. 平凡结构体

平凡结构体的特点是,它的类型信息不但含有各字段的类型信息,还包含了该类型的内存对齐值。

struct_pack认为具有不同对齐的平凡结构体是不同的类型,并可以在反序列化时安全的检查出错误。 例如:

cpp
#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宏注册的类型一定不是平凡结构体。 例如:

cpp
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

cpp
struct person_v2 {
  int age;
  std::string name;
  struct_pack::compatible<std::string> nick_name;
};

我们可以保证其向前兼容性:

cpp
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");

也可以保证其向后兼容性

cpp
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类型。

注意,在修改字段的过程中,如果想保证兼容性,必须遵循只增不改原则:

  1. 只增:新添加的字段必须是struct_pack::compatible<T, version_number>类型,且version_number必须大于上一次填入的版本号。
  2. 不改:不修改/删除任何一个旧的字段。

如果删去/修改了旧的struct_pack::compatible<T, version_number>类型,或者新增的struct_pack::compatible<T, version_number>类型版本号不是递增的,则两个结构体不能相互兼容,并且在两个不同版本之间序列化/反序列化的行为是未定义的!

struct_pack的类型信息和类型哈希

类型哈希校验

struct_pack的类型校验依赖于编译期获取的类型信息,并在编译期执行哈希计算,从而得到用于校验的类型哈希码。

struct_pack计算类型哈希的步骤如下:

  1. 通过静态反射,从待序列化的类型生成一棵struct_pack类型树。
  2. 在编译期递归遍历这颗树,生成与类型树对应的类型字符串。
  3. 在编译期计算字符串的32位MD5哈希值。

如下图所示:

对于结构体

struct dummy {
  int32_t id;
  double number;
  std::string str;
};

其最外层是一个结构体,结构体作为类型树的根节点,包含了三个子成员:idnumberstr。前两者是基本类型,因此是类型树的叶子节点。后者是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具有高效且安全的类型校验能力。

This website is released under the MIT License.