C++一直被诟病缺少原生反射能力。Java有getClass()和注解,C#有Attribute,而C++标准直到现在都没有提供正式的反射特性。这意味着我们无法在运行时通过字符串名称去访问一个类的成员变量,做序列化、ORM映射、配置绑定、属性编辑器这些功能时就得为每个类手写大量重复代码。其实借助模板元编程和宏,完全可以自己实现一套轻量级的反射框架,让成员属性自动完成绑定注册。本文就来详细拆解这套方案的实现原理和完整源码。

一、为什么需要属性注册:从序列化的痛点说起
假设有一个配置类,包含多个成员变量,我们希望把它和JSON、XML或者数据库字段做双向映射。没有反射时,最直接的办法就是为每个类手写转换函数:
struct Config {
std::string host;
int port;
bool debug;
};
// 手写序列化,字段一多就痛苦
void Serialize(const Config&c, Json&json) {
json["host"] = c.host;
json["port"] = c.port;
json["debug"] = c.debug;
}
void Deserialize(Config&c, const Json&json) {
c.host = json["host"];
c.port = json["port"];
c.debug = json["debug"];
}
这种写法的问题很明显:每加一个字段就要同步修改序列化和反序列化两处代码,漏改一处就出bug;字段名称以字符串形式散落在代码里,重命名成员变量时编译器不会给出任何提示;如果系统里有几十上百个类需要映射,重复劳动量非常可观。
反射要解决的核心问题,就是建立"成员变量名"到"成员指针"的映射表。一旦这张表建立起来,序列化、反序列化、属性面板、网络同步等所有需要按名称访问成员的功能,都可以基于统一的接口实现,只需遍历属性表即可,新增字段也只需在注册处加一行代码。
二、核心技术原理:成员指针与模板推演
2.1 成员指针是反射的基石
C++中有一个经常被忽视的特性——成员指针(member pointer)。int Config::*ptr表示"指向Config类中int类型成员的指针",它本身不绑定任何对象,只有在通过对象解引用时才生效:
int Config::*p = &Config::port; // 取成员指针 Config c; c.*p = 8080; // 通过成员指针写入 int v = c.*p; // 通过成员指针读取
成员指针的意义在于:它可以作为编译期常量保存下来,和字符串名称配对存入注册表。运行时拿到对象地址和成员指针,就能完成读写。这就是属性绑定的底层机制。
2.2 用模板擦除类型差异
不同成员变量的类型各不相同,int Config::*和std::string Config::*是两个完全不同的类型,没法直接放进同一个容器。解决办法是模板擦除:定义一个抽象基类持有成员名称,再用模板派生类保存具体类型的成员指针,通过虚函数提供统一的读写接口。
// 属性访问器基类:只依赖类型T
template <typename T>
struct PropertyBase {
virtual ~PropertyBase() = default;
virtual void* GetPtr(T&obj) const = 0;
};
// 模板派生类:保存具体成员指针
template <typename T, typename FieldType>
struct PropertyImpl : PropertyBase<T> {
FieldType T::*memberPtr;
PropertyImpl(FieldType T::*p) : memberPtr(p) {}
void* GetPtr(T&obj) const override {
return &(obj.*memberPtr);
}
};
这样,不管成员是int、double还是自定义结构体,注册表中保存的都是std::unique_ptr<PropertyBase<T>>,调用GetPtr拿到void*后再按需转换。类型信息可以在注册时一并记录到枚举或type_index中,方便序列化时判断如何处理。
三、完整实现:自动注册框架源码
3.1 注册表与注册宏
把属性信息和注册逻辑封装起来,再用宏简化使用。宏展开发生在编译期,成员指针作为模板参数推演,实现零运行时开销的绑定:
#include <string>
#include <vector>
#include <memory>
#include <functional>
#include <iostream>
#include <typeindex>
// 属性描述结构
template <typename T>
struct PropertyInfo {
std::string name;
std::unique_ptr<void*> placeholder; // 预留
};
// 属性访问器基类
template <typename T>
struct IProperty {
std::string name;
virtual ~IProperty() = default;
virtual void* Get(T&obj) = 0;
virtual const void* Get(const T&obj) = 0;
};
// 具体实现
template <typename T, typename FieldType>
struct Property final : IProperty<T> {
FieldType T::*ptr;
Property(const std::string&n, FieldType T::*p)
: ptr(p) { this->name = n; }
void* Get(T&obj) override { return &(obj.*ptr); }
const void* Get(const T&obj) override { return &(obj.*ptr); }
};
// 类级别的注册表
template <typename T>
struct TypeInfo {
static std::vector<std::unique_ptr<IProperty<T>>>&Props() {
static std::vector<std::unique_ptr<IProperty<T>>> props;
return props;
}
template <typename FieldType>
static void Register(const std::string&name, FieldType T::*ptr) {
Props().push_back(std::make_unique<Property<T, FieldType>>(name, ptr));
}
// 运行时按名称查找
static IProperty<T>* Find(const std::string&name) {
for (auto&p : Props()) {
if (p->name == name) return p.get();
}
return nullptr;
}
};
// BEGIN宏:注册函数只执行一次
#define REFLECT_BEGIN(CLASS) \
static void RegisterAll() { \
static bool registered = []() {
#define REFLECT_FIELD(CLASS, FIELD) \
TypeInfo<CLASS>::Register(#FIELD, &CLASS::FIELD);
#define REFLECT_END() \
return true; \
}(); \
(void)registered; \
}
几个设计细节值得说明。注册表用函数内静态变量实现,保证首次访问时初始化且线程安全(C++11起编译器保证)。REFLECT_BEGIN中的lambda只执行一次,避免重复注册。#FIELD宏运算符把成员名转成字符串,这样名称和指针永远保持一致,重命名时编译器会直接报错。
3.2 使用示例与运行时读写
struct Player {
std::string name;
int level = 1;
double hp = 100.0;
REFLECT_BEGIN(Player)
REFLECT_FIELD(Player, name)
REFLECT_FIELD(Player, level)
REFLECT_FIELD(Player, hp)
REFLECT_END()
};
int main() {
Player::RegisterAll();
Player p;
p.name = "Hero";
p.level = 25;
// 按名称读取
auto* prop = TypeInfo<Player>::Find("level");
if (prop) {
std::cout << "level = " << *(int*)prop->Get(p) << std::endl;
}
// 按名称写入
prop = TypeInfo<Player>::Find("hp");
if (prop) {
*(double*)prop->Get(p) = 88.5;
std::cout << "hp = " << p.hp << std::endl;
}
// 遍历所有属性,做通用序列化
for (auto&item : TypeInfo<Player>::Props()) {
std::cout << item->name << std::endl;
}
return 0;
}
基于这套遍历能力,写一个通用的JSON序列化函数只需十几行:遍历属性表,根据成员类型分发到对应的转换分支。所有注册过的类共用这一个函数,新增类不再需要写任何序列化代码。
四、方案扩展与局限性分析
4.1 支持读写器和权限控制
实际项目中经常需要只读属性、序列化时跳过某些字段。可以在Property中增加标志位,或者把直接成员指针替换为getter/setter函数指针。函数指针同样可以模板化保存:
template <typename T, typename FieldType>
struct FuncProperty final : IProperty<T> {
std::function<FieldType(const T&)> getter;
std::function<void(T&, FieldType)> setter;
// Get和Get.const通过getter/setter间接访问
};
这种做法牺牲一点性能,换来更灵活的访问控制,比如setter里可以做范围校验、触发变更事件,非常适合属性编辑器场景。
4.2 局限性与注意事项
这套方案也有固有限制。第一,它无法枚举类的继承关系,需要再配合一个保存基类指针的注册项手动串起来;第二,静态成员、位域成员无法用成员指针表示,需要另行处理;第三,宏侵入式地要求修改类定义,如果类不能改动(第三方库的类)就无能为力,此时可以考虑外部注册的方式,把成员指针登记在类外面。
另外要注意void*类型擦除的安全性:写入时必须确保类型完全匹配,建议在注册时额外保存std::type_index,写入前做校验,否则容易引入隐蔽的内存越界问题。C++26有望引入静态反射特性,届时这类手写框架可以逐步迁移到标准方案,但在那之前,模板元编程加宏的组合依然是工程上最实用的选择。