c++原生不支持反射因标准未引入编译期/运行时完整类型信息,仅提供有限RTTI;__PRETTY_FUNCTION__和宏通过编译期展开模拟字段枚举与类型名提取,支撑序列化等场景,但需手动同步、不支持动态类型与嵌套递归。

为什么C++原生不支持反射,但__PRETTY_FUNCTION__和宏能凑合用?
C++标准至今没引入运行时类型信息(RTTI)以外的反射能力,typeid和dynamic_cast只能解决最基础的类型识别,没法获取成员名、字段偏移或自动遍历结构体。所以实际中常用编译期技巧“模拟”:靠__PRETTY_FUNCTION__在gcc/clang里提取函数签名中的类型名,或用宏把字段声明和元信息一起登记。这些不是真反射,但足够支撑序列化、调试打印、简单配置绑定等场景。
关键限制在于——所有信息必须在编译期可见,不能动态加载新类型;一旦类型定义变更,宏注册部分必须同步改,否则static_assert会炸或行为未定义。
用宏注册结构体字段并生成to_json()的最小可行方案
核心思路是让每个结构体显式声明自己的字段表,宏负责展开成初始化列表和访问器。比如:
#define REFLECT_STRUCT(Name, ...) struct Name##_reflect { static constexpr auto fields() { return std::make_tuple(__VA_ARGS__); } }; template<> struct reflector : Name##_reflect {}; #define FIELD(name) std::make_pair(#name, &Name::name)
配合一个泛型to_json模板,遍历reflector里的std::pair,就能逐个取值拼JSON。注意三点:
立即学习“C++免费学习笔记(深入)”;
-
__VA_ARGS__里每个FIELD(x)必须严格对应结构体真实公有成员,私有成员无法通过指针访问 - 字段顺序影响JSON键序,宏展开后不可逆,调试时看预处理输出(
g++ -E)最靠谱 - 不支持嵌套结构体自动递归,得手动为子类型也写
REFLECT_STRUCT
模板+特化实现类型名字符串化,比typeid(T).name()可靠
typeid(T).name()返回的是编译器特定的mangled名,abi::__cxa_demangle又依赖libstdc++,跨平台易崩。更稳的方式是模板特化:
template constexpr auto type_name = "unknown"; template<> constexpr auto type_name = "int"; template<> constexpr auto type_name = "std::string"; // ... 手动覆盖常用类型
这招在编译期就确定字符串字面量,无运行时开销,且可读性直接拉满。缺点也很明显:
- 每新增一个自定义类型,必须手动加一条
template特化 - 无法处理模板实例化类型如
vector,除非你愿意写template(C++20起支持字符串拼接)constexpr auto type_name > = "vector + ">" - 别想用
decltype自动推导——constexpr上下文里不能调用非constexpr函数
宏反射的致命坑:__LINE__冲突、头文件重复包含、调试信息错位
很多人用__LINE__生成唯一变量名防重定义,但一旦宏在头文件里被多个源文件包含,__LINE__值不同会导致符号不一致,链接时报undefined reference。真正安全的做法是用__COUNTER__(MSVC/g++均支持)或__FILE__哈希。
另一个隐形雷区是调试体验:宏展开后的代码行号全乱,GDB里单步会跳到预处理后的临时行,看不到原始字段声明。如果项目要长期维护,务必在构建脚本里加-save-temps,保留.ii文件查问题。
最后提醒一句:所有宏反射方案在Clangd、IntelliSense这类语言服务器里基本失能,补全和跳转大概率失效——别指望ide能帮你检查字段名拼错。