C++原生无反射因标准未支持运行时元数据操作,宏通过编译期展开硬编码字段名、类型等信息模拟;REFLECTABLE宏需显式列出字段并生成静态字符串数组与访问器,offsetof仅适用于标准布局类型,成员指针更安全但有开销;序列化中字段名必须为静态字面量以避免生命周期问题。

为什么 C++ 原生没有反射,但宏能“模拟”?
C++ 标准至今(C++20/C++23)不提供运行时类型信息(RTTI)以外的反射能力,typeid 和 dynamic_cast 只能做有限的类型识别和安全转换,无法枚举类成员、获取字段名或自动调用任意成员函数。所谓“宏模拟反射”,本质是**在编译期用宏展开生成重复模式代码**,把本该由语言内置支持的元数据(如字段名、类型、偏移)硬编码进程序里。
它不是真正的反射——没有运行时解析、不支持动态加载类、不能处理未在宏中显式声明的成员。但它足够轻量,适合配置驱动、序列化、调试打印等固定结构场景。
REFLECTABLE 宏怎么写?关键在字段列表与访问器生成
核心思路:用一个宏接收类名 + 字段列表,为每个字段生成名称字符串、类型、getter/setter 绑定(通常返回 std::tuple 或自定义结构体)。常见写法依赖可变参数宏和预处理器计数技巧。
- 字段列表必须显式写出:
REFLECTABLE(MyStruct, (int, a), (double, b), (std::string, name)) - 宏内部用
BOOST_PP(Boost.Preprocessor)或自研递归宏展开每一项,生成类似static constexpr const char* names[] = {"a", "b", "name"}; - 每个字段需配套生成
get_field()、set_field(val)等模板函数,靠std::get(std::tie(...))或offsetof实现访问 - 注意:非 POD 类型字段(含构造/析构)不能直接用
offsetof;此时必须用引用绑定或成员指针,否则 UB
用宏模拟反射时,offsetof 和成员指针哪个更安全?
offsetof 快但限制多:仅适用于标准布局类型(standard-layout),且字段不能是引用、非静态成员函数、有虚函数的类。一旦类加了 virtual 或继承,offsetof 结果不可靠。
立即学习“C++免费学习笔记(深入)”;
成员指针(decltype(&T::field))无此限制,可跨继承体系使用,但开销略高(存储额外指针),且不能直接算偏移,需配合对象实例调用:
struct A { int x; };
struct B : A { double y; };
auto ptr = &B::x; // 合法,指向基类成员
B b;
int& ref = b.*ptr; // 正确访问
实际项目中,若只处理简单数据结构(如配置结构体),offsetof 更简洁;若需兼容复杂类,优先用成员指针 + 模板特化。
宏反射在序列化中的典型误用:字符串字面量 vs 运行时拼接
很多人用宏生成字段名数组后,直接拼接 JSON 键名,却忽略字符串生命周期:
- 错误写法:
const char* key = "name"; sprintf(buf, "\"%s\":%d", key, obj.name);—— 看似没问题,但若宏展开中用了临时字符串(如#field在某些上下文被截断),或 key 指向栈内存,就崩溃 - 正确做法:所有字段名必须是静态存储期字符串,即宏内用
"a"、"b"字面量,或存入static constexpr std::array - 别试图在宏里做
std::string("key" + std::to_string(i))—— 预处理器不执行 C++ 表达式,这种写法根本过不了编译
真正难的不是生成名字,而是确保所有元数据在编译期固化、零运行时分配、且与对象内存布局严格对齐。稍有松动,序列化结果就错位或越界。











