如何在编译期确保序列化函数生成预期大小的缓冲区?
紧凑二进制序列化的安全实现方案
当前方案的痛点
我们目前使用带位域的packed结构体结合联合来处理二进制序列化:
struct Data1 { uint8_t type; uint8_t value; bool aBool:1; uint8_t threeBits:3; uint8_t spare:4; } __attribute__((packed)); const uint8_t Data1SerializedSize{3}; static_assert(sizeof(Data1) == Data1SerializedSize, "wrong Data1 size"); // ... const uint8_t MaxBufferSize{32}; union AllDatas { Data1 data1; // ... 其他数据结构 uint8_t buffer[MaxBufferSize]; }__attribute__((packed)); static_assert(sizeof(AllDatas) <= MaxBufferSize);
通过联合直接访问字节数组的方式实现序列化:
void send(Serial& serial, const Data1& data) { AllDatas u; u.data1 = data; serial.write(u.buffer, Data1SerializedSize); }
这个方案能运行的前提是统一使用相同CPU架构,且GCC允许访问联合中未最后写入的成员,但存在明显问题:
- 违反C++标准,属于未定义行为
- 严重依赖编译器和平台实现
- 代码可读性差,位域打包逻辑隐含在结构体定义中,不易维护
唯一的优势是能通过static_assert在编译期检查结构体大小是否符合预期。
安全序列化的实现要求
我们需要实现手动序列化函数,满足:
- 编译期确保输出缓冲区大小符合预期,不会出现长度不匹配
- 明确处理每个成员,避免遗漏或重复打包
- 平台无关,符合C++标准
- 避免重复声明输出数组的类型和大小
解决方案实现
1. 编译期校验序列化长度
通过constexpr偏移量跟踪每个成员的序列化位置,结合static_assert在编译期验证总长度是否匹配预期。同时手动处理位域的打包逻辑,完全控制二进制格式:
#include <array> #include <cstdint> constexpr uint8_t Data1SerializedSize = 3; // 定义类型别名,避免重复声明数组类型 using Data1SerializedBuffer = std::array<uint8_t, Data1SerializedSize>; struct Data1 { uint8_t type; uint8_t value; bool aBool:1; uint8_t threeBits:3; uint8_t spare:4; }; Data1SerializedBuffer serialize(const Data1& data) { Data1SerializedBuffer buffer{}; // 编译期定义每个成员的偏移量 constexpr size_t offset_type = 0; constexpr size_t offset_value = offset_type + 1; constexpr size_t offset_bitfield = offset_value + 1; // 编译期校验总长度 static_assert(offset_bitfield + 1 == Data1SerializedSize, "Data1 serialization length does not match expected size"); // 序列化普通成员 buffer[offset_type] = data.type; buffer[offset_value] = data.value; // 手动打包位域,先掩码确保数据在有效范围内,避免垃圾位干扰 uint8_t bitfield_byte = 0; bitfield_byte |= (static_cast<uint8_t>(data.aBool) & 0x1); bitfield_byte |= ((data.threeBits & 0b111) << 1) & 0b1110; bitfield_byte |= ((data.spare & 0b1111) << 4) & 0b11110000; buffer[offset_bitfield] = bitfield_byte; return buffer; }
2. 避免数组类型重复声明
有两种方式解决这个问题:
- 类型别名:如上面定义的
Data1SerializedBuffer,统一使用别名声明返回类型和函数内的变量 - C++17返回类型推导:利用
auto让编译器自动推导返回类型,函数体内只需声明一次数组:constexpr auto serialize(const Data1& data) { std::array<uint8_t, Data1SerializedSize> buffer{}; // ... 序列化逻辑 ... return buffer; }
3. 避免遗漏成员的保障
虽然C++没有编译期强制检查结构体成员是否全部处理,但可以通过以下方式降低风险:
- 序列化代码与结构体成员一一对应,添加注释标记每个成员的处理位置
- 若结构体成员有增减,编译期的
static_assert会因偏移量总和不匹配而触发错误,间接提醒开发者更新序列化逻辑
方案优势
- 标准合规:完全符合C++标准,无未定义行为
- 平台无关:手动控制位域打包逻辑,不受编译器、CPU架构影响
- 编译期校验:通过
static_assert确保序列化长度符合预期,提前发现错误 - 代码清晰:序列化逻辑明确可见,易于维护和调试
内容的提问来源于stack exchange,提问作者Bertrand Thelen
相关产品推荐
相关产品推荐

