C++标准库无反射,需宏+模板(编译期)或手动注册(运行时)实现;前者零开销但侵入强,后者灵活但有成本;std::any/variant不足以支撑通用反射。

纯 C++ 标准库不提供反射能力,typeid 和 type_info 只能获取类型名(且不可靠),无法枚举成员、调用方法或访问字段。要实现“简单反射”,必须借助宏 + 模板的编译期元编程,或手动注册 + 运行时结构体(如 map)模拟。二者不是替代关系,而是适用场景不同:编译期方案零运行时开销但侵入性强;运行时方案灵活但需手动维护、有内存和查表成本。
用宏 + 模板生成编译期类型信息(C++17 起可行)
核心思路是让每个类通过宏声明其可反射的字段,宏展开为静态成员函数(如 get_field_names())、字段访问器(如 get_field_value(this, "x")),并利用模板参数包推导字段类型与顺序。
典型陷阱是宏展开后作用域污染、字符串字面量生命周期(不能返回局部 char*)、以及 MSVC 对模板递归深度限制更严。
- 必须用
constexpr std::string_view或static constexpr const char[]存储字段名,避免运行时构造 - 字段访问器函数需统一签名,例如
std::any get_field(const void* obj, std::string_view name) const,内部用 if-else 或折叠表达式匹配 - 推荐使用
BOOST_PP或自研小宏(如REFLECT(x, y, z))来减少重复模板代码,但注意宏嵌套层级不宜超 10 层 - C++20 的
reflexpr(P0194)仍属 TS 未进标准,不可依赖
struct Point {
int x = 0;
float y = 0.0f;
REFLECT_FIELDS(x, y) // 展开为 static constexpr auto fields = std::make_tuple(&Point::x, &Point::y);
};
// 使用示例(伪代码,实际需完整宏定义)
auto p = Point{1, 2.5f};
auto val = Point::get_field(&p, "x"); // 返回 std::any 包裹的 int
手动注册的运行时反射(轻量、跨平台、无宏)
适合不想改原有类定义、或需动态加载类型(如插件系统)的场景。本质是为每个类型维护一个全局 std::unordered_map<:string fieldinfo>,FieldInfo 包含 offset、type_id、getter/setter 函数指针。
立即学习“C++免费学习笔记(深入)”;
关键难点不在注册本身,而在如何安全获取字段偏移——不能直接用 offsetof 非 POD 类型(C++17 要求标准布局),也不能对虚继承类使用。
- 只对
std::is_standard_layout_v为 true 的类型启用offsetof,否则强制要求用户传入 lambda getter/setter - 注册函数应为线程安全,可用
std::call_once+ 静态局部变量保证单例初始化 - 字段名作为 key,必须全局唯一(建议加类型前缀,如
"Point.x"),否则跨类型查询会冲突 - 避免在
main()之前调用反射注册(静态对象构造顺序不确定)
// 注册示例(简化版)
struct Point { int x; float y; };
REFLECT_TYPE(Point)
.field("x", offsetof(Point, x), typeid(int))
.field("y", offsetof(Point, y), typeid(float));
为什么 std::any / std::variant 不足以支撑反射?
std::any 解包需知道确切类型(any_cast),而反射查询常只有名字;std::variant 要求编译期穷举所有可能类型,无法应对未知结构(如 JSON 映射到任意 struct)。
真实项目中,反射值容器往往需要三层抽象:存储层(void* + size)、类型层(std::type_info* 或自定义 TypeId)、语义层(是否可序列化、是否为容器等标记)。漏掉任一层,都会导致下游(如序列化、调试器显示)无法可靠工作。
- 不要用
std::any直接存非拷贝类型(如std::unique_ptr),移动后原any置空,易引发未定义行为 - 若支持函数反射(如调用成员函数),必须额外处理
this指针绑定和 const 限定符,std::function无法表示成员函数签名差异 - 调试器友好性常被忽略:GDB/LLDB 不识别自定义反射结构,需配合
.gdbinit脚本或 Python 扩展才能打印
真正难的不是“怎么写一个能跑的反射”,而是“怎么让反射不成为性能瓶颈、不破坏封装、不引入隐式依赖、还能被工具链理解”。多数中小型项目,用 JSON Schema + 代码生成(如 protobuf + protoc)比手写反射更稳。自己造轮子前,先确认 magic_get(Boost.PFR)是否满足需求——它用 C++17 结构化绑定实现零宏、零运行时注册的编译期反射,已覆盖 80% 常见用例。










