在C++项目里,我们经常希望像C#那样给类或方法打上“标签”,然后在运行时根据标签做序列化、依赖注入或自动化测试注册。标准C++没有attribute和反射,但借助宏、模板以及静态初始化顺序可控的技巧,完全可以模拟出一套可用的自定义注解系统。

一、核心设计思路
模拟Attribute的本质是把“附加在类型或函数上的元信息”在编译期收集起来,存放到一个全局注册表中。由于C++没有运行时的类型自省,我们必须让每个被注解的类在编译期主动“登记”自己。最常见做法是定义一个基类或模板结构保存元信息,然后用宏在类内部插入静态成员变量,该变量在程序启动前完成构造并注册到全局map里。
这种方案的好处是零运行时开销:注册发生在main之前,查询只是一次map查找。缺点是必须显式使用宏,且无法获取私有成员细节。但它足以覆盖大部分业务框架需要的“类级别标签”与“可动态创建”能力。我们将通过三个部分展开:Attribute宏定义、类型注册表、以及反射调用示例。
1.1 定义Attribute载体
我们首先抽象一个Attribute基类,所有自定义注解都从它派生。每个注解可以携带字符串或整型参数,例如“表名”“版本号”。为了让注解能附着到类上,我们不在类外部写装饰器,而是让类内部包含一个静态的注解实例列表。
下面代码展示了最基础的Attribute结构与宏,用来声明一个类属于“可序列化”并带有名称。注意宏里利用了匿名命名空间与静态变量,确保不同编译单元不会冲突。
#include <string>
#include <vector>
#include <map>
// 注解基类
struct Attribute {
virtual ~Attribute() = default;
std::string name;
};
// 具体注解:表名
struct TableAttribute : Attribute {
std::string table_name;
};
// 全局注册表:类型id -> 注解列表
inline std::map<int, std::vector<Attribute*>>& Registry() {
static std::map<int, std::vector<Attribute*>> reg;
return reg;
}
// 宏:给类附加Table注解
#define ATTR_TABLE(Class, tbl)
static struct {
TableAttribute attr;
int dummy;
} _attr_##Class = [](){
TableAttribute a; a.name = "Table"; a.table_name = tbl;
Registry()[typeid(Class).hash_code()].push_back(new TableAttribute(a));
return decltype(_attr_##Class){a, 0};
}();
1.2 类型注册与动态创建
仅有注解还不够,反射通常还要求“给名字就能new对象”。因此我们引入一个工厂函数指针表,配合前面的宏,让类在注册注解的同时登记创建函数。这样框架就能通过字符串找到类型并实例化。
以下扩展代码在刚才基础上增加创建器。宏同时完成注解挂载和工厂登记,用户只需写一行REGISTER_CLASS即可。由于使用了typeid,需要开启RTTI;若项目禁用RTTI,可用__COUNTER__生成唯一id替代。
#include <functional>
inline std::map<std::string, std::function<void*()>>& Factory() {
static std::map<std::string, std::function<void*()>> f;
return f;
}
#define REGISTER_CLASS(Class, tbl)
ATTR_TABLE(Class, tbl)
static int _reg_##Class = [](){
Factory()[#Class] = [](){ return new Class(); };
return 0;
}();
// 示例业务类
class User {
public:
int id;
std::string name;
};
REGISTER_CLASS(User, "t_user")
二、读取注解与反射调用
当类完成注册后,任意地方都能通过类型哈希或类名查询注解内容和创建实例。下面示例演示如何遍历某类型的注解,以及通过工厂生成对象。这种写法在ORM层自动建表、配置绑定中非常实用。
需要提醒的是,静态变量初始化顺序在跨编译单元时并不确定,但因为我们只在Registry()内部用函数局部静态变量,它遵循“首次调用时初始化”的规则,所以多文件注册是安全的。如果注解参数需要在构造期计算,务必保证依赖项已可用。
2.1 查询注解信息
我们通过类型哈希从Registry提取vector,再向下转型拿到具体注解。如果找不到,说明该类未用宏声明,可给出编译期或运行期警告。下面的函数封装了查询表名的过程。
#include <typeindex>
std::string GetTableName(const std::type_index& ti) {
auto it = Registry().find(ti.hash_code());
if (it == Registry().end()) return "";
for (auto* a : it->second) {
if (auto* t = dynamic_cast<TableAttribute*>(a)) {
return t->table_name;
}
}
return "";
}
// 使用
#include <iostream>
void demo() {
std::cout << GetTableName(typeid(User)) << std::endl; // 输出 t_user
void* obj = Factory()["User"]();
delete (User*)obj;
}
2.2 方案优缺点分析
上述模拟方式的优势在于完全基于标准C++,不依赖第三方库,注解与工厂一体,适合嵌入式或游戏引擎这类不能引入庞大框架的环境。同时,由于注册在main前完成,运行时性能等同于普通map查找。
不足之处是语法不够优雅,需要宏入侵类定义;且无法自动枚举类的成员变量(除非再写字段注册宏)。若项目需要完整字段级反射,建议结合开源库如RTTR或Qt的Meta-Object。但对于“自定义Attribute标签+按名建对象”的需求,本方案已经足够轻量清晰。
三、总结与扩展
通过宏驱动静态注册,我们让C++拥有了类似自定义Attribute与基础反射的能力。核心就是全局map加函数局部静态变量保证初始化安全。读者可进一步扩展:支持方法级注解、参数注解,或把hash_code替换为稳定字符串id以兼容关闭RTTI的构建。
如果后续需要序列化,只需在类里再附加字段映射宏,把每个成员的偏移与名称写入另一个注册表,即可实现自动转JSON。这种渐进式扩展正是模拟反射机制的魅力所在。
C++reflectioncustom_attribute修改时间:2026-08-02 15:06:32