C++纯标准不支持运行时反射,所谓“模拟”是通过模板元编程、静态注册和类型擦除在编译期建立类型与元信息映射,适用于序列化等有限场景,但无法替代C#动态特性。

纯 C++ 标准不支持运行时反射,也无原生 Attribute 或注解语法;所谓“模拟”,本质是用模板元编程 + 静态注册 + 类型擦除,在编译期建立类型与元信息的映射关系——它不能替代 C# 的 [Serializable] 那种动态行为,但能支撑序列化、配置绑定、单元测试发现等有限场景。
为什么不能直接用宏定义 + RTTI 模拟 Attribute?
RTTI(typeid、dynamic_cast)只提供最基础的类型名和继承关系,无法携带用户自定义字段(如 name="user_id"、required=true)。宏虽可注入代码,但无法生成类型安全的访问接口,且宏展开后无法被模板推导识别——这意味着你写 REFLECTED_STRUCT(User),后续却没法在 for_each_field 中自动获取每个字段的注解值。
可行路径只有一条:让每个字段的元信息成为编译期常量,并通过特化模板或 constexpr 数据结构固化下来。
如何用模板特化 + constexpr 结构体注册字段级注解?
核心是把“字段 + 注解”打包成一个编译期实体,再用偏特化为每个类型建立映射。例如:
struct FieldMeta {
constexpr FieldMeta(const char* n, bool r) : name(n), required(r) {}
const char* name;
bool required;
};
<p>template<typename t int i> struct field_meta;</typename></p><p>// 手动为 User::id 注册
template struct field_meta<user> {
static constexpr FieldMeta value{"id", true};
};</user></p><p>// 手动为 User::name 注册<br>
template struct field_meta<user> {
static constexpr FieldMeta value{"username", false};
};</user></p>
这样 field_meta 就是编译期字符串,可参与 if constexpr 分支,也可用于生成 JSON key。缺点是注册完全手动,易出错、难维护。
- 必须保证字段顺序与偏特化索引严格一致(改成员顺序就崩)
- 无法自动推导字段类型,
field_type还得另写一套特化 - 没有统一入口获取字段总数,得靠
num_fields额外定义::value
怎样让反射注册接近“声明即注册”,减少手写特化?
借助 BOOST_PP 或 C++20 的 __VA_OPT__ + 宏拼接,可将注册逻辑收敛到类定义附近。典型模式如下:
#define REFLECTED_FIELD(name, ...) \
static constexpr auto _meta_##name = FieldMeta{#name, __VA_ARGS__};
<p>struct User {
int id;
std::string name;
REFLECTED_FIELD(id, true);
REFLECTED_FIELD(name, false);
};</p>
但这只是“视觉上”靠近,仍需额外模板提取这些 _meta_* 静态变量。真正减轻负担的做法是:用一个全局注册表(constexpr 数组)配合宏展开填充,再用 std::tuple 或 std::array 存储所有字段元信息。
- 宏展开必须在类定义之后、使用前完成,否则
static_assert会因未定义而失败 - C++20 起可用
consteval函数校验字段名是否真实存在(通过 SFINAE 检查decltype(T{}.name)),但无法跨翻译单元做一致性检查 - 所有元信息必须是字面量类型(
const char*可,std::string不可),否则进不了constexpr上下文
运行时怎么拿到某个对象的字段注解并遍历?
没有真正的运行时反射,所以“遍历”其实是编译期展开 + 运行时回调。典型实现是写一个接受泛型 Lambda 的 for_each_field 函数模板:
template<typename t>
void for_each_field(const T& obj, auto&& f) {
if constexpr (has_reflection_v<t>) {
f(obj.id, field_meta<t>::value);
f(obj.name, field_meta<t>::value);
}
}</t></t></t></typename>
调用时传入 lambda,它会在编译期被实例化为多个独立调用,每次传入对应字段值和其 FieldMeta。这避免了虚函数或函数指针跳转,性能接近手写,但代价是二进制体积随反射类型数量线性增长。
- lambda 参数类型必须能匹配字段实际类型(
int和const std::string&不同),否则 SFINAE 失败 - 无法对字段做统一容器存储(比如全转成
std::any),因为类型擦除会丢失constexpr属性 - 若字段含引用、位域、静态成员,该方案直接失效——它们无法作为
f(obj.field, meta)的参数转发
这套方案的边界非常清晰:它只适用于 POD 或简单聚合类型,且所有反射需求必须在编译期可知。一旦涉及插件热加载、JSON Schema 动态校验、或字段名运行时拼接,就得引入外部 DSL 或代码生成工具——模板元编程在这里不是银弹,而是精确控制的手术刀。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











