C++11无符号整数序列化循环展开模板合理性及编译占比咨询
C++模板递归实现无符号整数序列化相关问题
问题描述
我接触C++模板的时间尚短,目前还无法熟练运用该特性,因此提出以下问题。
我编写了一套用于无符号整数跨平台序列化的循环展开模板,模板代码如下:
template <class tUnsignedIntegerType, const uint8_t tByteIndex> class SerializerT { public: inline static void serialize (const tUnsignedIntegerType fUnsignedInt, uint8_t (&fPayLoadDestinationBuffer)[sizeof(tUnsignedIntegerType)]) { // Serialize the byte in MSB -> LSB order fPayLoadDestinationBuffer[tByteIndex] = (uint8_t) (0xff & (fUnsignedInt >> (tByteIndex << 3))); // << 3 == *8 // Serialize the next byte in MSB -> LSB order SerializerT<tUnsignedIntegerType, tByteIndex - 1>::serialize(fUnsignedInt, fPayLoadDestinationBuffer); } }; template <class tUnsignedIntegerType> class SerializerT<tUnsignedIntegerType, 0> { public: inline static void serialize (const tUnsignedIntegerType fUnsignedInt, uint8_t (&fPayLoadDestinationBuffer)[sizeof(tUnsignedIntegerType)]) { // Serialize the LSB byte fPayLoadDestinationBuffer[0] = (uint8_t) (0xff & fUnsignedInt); } };
对外提供的用户接口如下:
template <class tUnsignedIntegerType> inline void serializeInt(const tUnsignedIntegerType fInt, uint8_t (&fPayLoadDestinationBuffer)[sizeof(tUnsignedIntegerType)]) { SerializerT<tUnsignedIntegerType, sizeof(tUnsignedIntegerType) - 1>::serialize(fInt, fPayLoadDestinationBuffer); }
简单使用示例:
const uint64_t u64 = 0x0ff0ffff0000f00f; uint8_t u64PayLoad[8]; serializeInt(u64, u64PayLoad); // u64PayLoad now holds the serialized int in LSB->MSB order starting at index 0.
目前该实现可正常运行,我想咨询两个问题:
- 与采用普通运行时循环、由编译器自动完成循环展开的实现相比,当前这种模板递归展开的实现是否属于过度设计?
- 这套序列化逻辑中有多少操作是在编译期完成的?我已知对
fPayLoadDestinationBuffer[tByteIndex]的赋值操作是在运行时执行,其余逻辑的执行阶段是怎样的?
补充说明:当前使用的C标准版本为C11。
回答
直接针对两个问题说结论:
编译期/运行时逻辑划分
除了你已经明确的数组元素赋值操作是运行时执行,其余绝大多数逻辑都在编译期完成:
- 全部模板实例化工作在编译期收尾:编译器会根据传入的无符号整数类型,递归生成从
sizeof(tUnsignedIntegerType)-1到0每一个索引值对应的SerializerT::serialize静态函数实例,tByteIndex=0的特化终止版本也会在编译期完成匹配,整个递归展开过程没有运行时的递归调用开销。 - 移位运算的偏移量是编译期常量:
tByteIndex << 3(等价于乘8)的计算结果在编译阶段就会被替换成固定立即数,运行时不需要额外做这部分算术计算。 - 缓冲区长度校验在编译期完成:接口用数组引用传参,只要传入的输出缓冲区长度和目标整数类型的字节数不匹配,编译阶段就会直接报错,不会把长度不匹配的问题留到运行时触发内存越界。
- 函数内联展开在编译期决策:开O2及以上优化等级时,所有递归生成的静态函数调用会被编译器完全消掉,最终生成的汇编就是连续的按字节取值、写入对应数组位置的指令序列,和手动逐行给每个数组元素赋值的生成代码完全一致,没有额外调用开销。
运行时实际执行的操作只有两类:一是将传入的整数按编译期确定的固定移位量做移位、和0xff做与运算取出对应字节;二是把取出的字节写入数组对应位置。
是否属于过度设计
在C11环境下,这个实现不属于过度设计。
C11没有C14的通用constexpr、也没有C17的折叠表达式,想要保证固定长度的序列化逻辑被完全展开,模板递归是最稳妥的实现手段。如果换成普通for循环写运行时逻辑,即使开优化,编译器也不会100%保证循环展开——不同编译器、不同优化等级的展开策略差异很大,遇到1、2字节的小整数可能展开,遇到8字节甚至更长的自定义整数类型,就可能保留循环分支、计数器增减的额外开销。
而这个模板递归版本,只要编译器支持C11,就能保证固定次数的序列化逻辑全部展开,没有循环分支开销,还额外获得了编译期缓冲区长度校验的安全性,收益是明确的。如果后续升级到更新的C标准,可以用折叠表达式、std::byte等特性把代码写得更简洁,但在C++11的约束下,这个实现是合理且实用的。
内容的提问来源于stack exchange,提问作者Nilsie
相关产品推荐
相关产品推荐

