编译时基于成员列表计算类/结构体大小并校验成员完整性
编译时校验类成员是否全部传入以计算对象总大小
问题背景
我们需要实现一个对象内存占用统计系统:现有calcClassSize函数接收类型S的对象及所有S的成员参数包,需为每个成员调用getExtraUsedMemory(field)统计堆内存占用,但存在遗漏成员的风险。
目标是编译时校验无成员遗漏:实现编译时函数calcMembersSize,接收成员参数包,计算该列表对应的对象总大小并与sizeof(S)对比。需要考虑虚表指针和基类子对象(因此Boost.PFR不适用,因其要求标准布局)。
前提假设
- 成员按
S声明顺序传入 - 使用C++20标准
- 仅适配GCC13编译器
- 无虚拟基类及自定义
alignas成员 - 可接受形式上的未定义行为(后续迁移至C++26反射)
实现方案
可以实现,核心思路是利用GCC编译时特性,通过成员指针计算偏移量,推导整个对象的总大小,从而校验成员是否全部传入。
核心原理
- 成员偏移量计算:利用GCC编译时对空指针解引用的优化(形式上的UB),获取成员在对象中的偏移量,包含基类子对象和虚表指针的占用空间
- 结束位置跟踪:按传入顺序计算每个成员的结束位置(偏移量 + 成员类型大小),取所有成员的最大结束位置
- 对齐补全:将最大结束位置调整为对象对齐要求的整数倍,得到的结果与
sizeof(T)对比,验证成员是否完整
完整代码实现
#include <iostream> #include <type_traits> // 从成员指针提取类和成员类型的辅助 trait template<typename MemPtr> struct mem_ptr_traits; template<typename T, typename Mem> struct mem_ptr_traits<Mem T::*> { using class_type = T; using member_type = Mem; }; template<typename MemPtr> using mem_ptr_class_t = typename mem_ptr_traits<MemPtr>::class_type; template<typename MemPtr> using mem_ptr_member_t = typename mem_ptr_traits<MemPtr>::member_type; // 计算单个成员的结束偏移(偏移量 + 成员大小) template<typename MemPtr> consteval size_t member_end_offset(MemPtr mem_ptr) { using T = mem_ptr_class_t<MemPtr>; using Mem = mem_ptr_member_t<MemPtr>; // 利用GCC编译时优化:空指针解引用仅计算偏移,无实际内存访问 size_t offset = reinterpret_cast<size_t>(&(reinterpret_cast<T*>(0)->*mem_ptr)); return offset + sizeof(Mem); } // 递归计算所有成员的最大结束偏移 template<typename... MemPtrs> consteval size_t calc_max_end_offset() { return 0; } template<typename FirstMemPtr, typename... RestMemPtrs> consteval size_t calc_max_end_offset(FirstMemPtr first, RestMemPtrs... rest) { size_t current_end = member_end_offset(first); size_t rest_max = calc_max_end_offset(rest...); return current_end > rest_max ? current_end : rest_max; } // 计算对象总大小:最大结束偏移 + 对齐补全 template <typename... MemPtrs> consteval size_t calcMembersSize(MemPtrs... memptrs) { using T = mem_ptr_class_t<FirstMemPtr>; // 校验所有成员属于同一类 static_assert((std::is_same_v<mem_ptr_class_t<MemPtrs>, T> && ...), "All members must belong to the same class"); size_t max_end = calc_max_end_offset(memptrs...); size_t align = alignof(T); // 计算对齐所需的补全空间 size_t padding = (align - (max_end % align)) % align; return max_end + padding; } // 测试用例结构 struct B { virtual void v() {} }; struct C { virtual void w() {} }; struct D { int j; }; struct A : B, C, D { int i; double d; short s; bool b; std::string str; char c; }; // 内存统计主函数,包含编译时校验 template <typename T, typename... MemPtrs> size_t calcClassSize(const T& object, MemPtrs... memptrs) { static_assert(calcMembersSize(memptrs...) == sizeof(T), "Missing class members"); size_t total = 0; // 模拟统计逻辑:成员自身大小 + 额外堆内存占用 ((total += sizeof(object.*memptrs) + /* getExtraUsedMemory(object.*memptrs) */ 0), ...); return total; } int main() { // 编译时校验:传入全部成员时大小匹配 static_assert(calcMembersSize(&A::i, &A::d, &A::s, &A::b, &A::str, &A::c) == sizeof(A)); // 编译时校验:遗漏成员时大小不匹配 static_assert(calcMembersSize(&A::d, &A::s, &A::c, &A::b, &A::str) != sizeof(A)); static_assert(calcMembersSize(&A::i, &A::s, &A::c, &A::b, &A::str) != sizeof(A)); static_assert(calcMembersSize(&A::i, &A::d, &A::c, &A::b, &A::str) != sizeof(A)); static_assert(calcMembersSize(&A::i, &A::d, &A::s, &A::str) != sizeof(A)); A a; std::cout << "Total calculated size: " << calcClassSize(a, &A::i, &A::d, &A::s, &A::b, &A::str, &A::c) << std::endl; std::cout << "All static assertions passed!" << std::endl; return 0; }
关键细节说明
- 成员指针参数:避免依赖运行时对象,确保能在
consteval中完成编译时计算 - 空指针解引用优化:GCC在编译时会直接计算成员偏移量,不会生成实际的内存访问操作,符合前提中对形式上UB的容忍
- 顺序依赖:要求成员按声明顺序传入,确保能正确跟踪每个成员的位置,避免因内存布局的间隙导致计算错误
- 对齐补全:对象总大小必须是其对齐要求的整数倍,最后一步的补全逻辑保证计算结果与
sizeof(T)完全一致
内容的提问来源于stack exchange,提问作者Alex Jenter
相关产品推荐
相关产品推荐

