资讯详情

C++ 反射机制详解及实例代码

📅 2026/11/4 7:46:44 | 华诺云谱 👁 阅读
C++ 反射机制详解及实例代码
前言反射reflection指程序在运行时检查自身的类型、成员、函数并据此创建对象或调用方法。Java、C# 有完整的反射标准 C 至今没有。C 只有一套很有限的运行期类型信息run-time type information, RTTI能做类型识别和安全的向下转换但不能枚举一个类有哪些成员、不能按名字调用任意函数、不能根据字符串创建任意类型。常见的误解有三个。第一把typeid当成反射以为typeid(obj).name()能拿到可用的类名并据此创建对象——它返回的往往是编译器修饰过的名字且标准不保证内容。第二以为dynamic_cast能替代反射实际上它只能在已知目标类型的前提下做转换不能从字符串反推类型。第三以为 C26 的反射提案已经是可用标准实际上截至本文写作时它尚未成为已发布的正式标准各编译器的支持情况需要单独确认。工程上所谓的「C 反射」通常指手动注册 查表在程序启动时把「名字 → 创建函数」「名字 → 字段访问器」登记到一张表里运行时按名字查表。本文给出这套做法的完整示例并说明它的边界。一、RTTI 能提供什么RTTI 的两个主要工具是typeid和dynamic_cast。typeid作用于多态类型的左值时返回动态类型的std::type_info作用于非多态表达式时返回静态类型。type_info::name()返回的字符串由实现决定常见的是经过名称修饰name mangling的形式不能直接当类名用。type_info支持比较也可以用来做类型索引。// 适用于 C17#include iostream#include typeinfostruct Base {virtual ~Base() default;};struct Derived : Base {};int main() {Derived d;Base b d;std::cout typeid(b).name() \n; // 动态类型 Derivedstd::cout typeid(Base).name() \n; // 静态类型 Baseif (dynamic_castDerived*(b) ! nullptr)std::cout b really refers to Derived\n;}dynamic_castDerived*(p)在p为空时返回空指针转换失败也返回空指针。转换成引用形式dynamic_castDerived(r)失败时抛std::bad_cast。它要求源类型是多态类型至少有一个虚函数否则编译不通过。RTTI 可以被编译器选项关闭例如 GCC / Clang 的-fno-rtti关闭后typeid和dynamic_cast对多态类型不可用。一些性能敏感或嵌入式项目会这样做这也是「不能依赖 RTTI 做核心架构」的原因之一。二、注册表用字符串创建对象最常用的「反射」是工厂注册每个类在启动时把自己的名字和一个创建函数登记到全局表调用方只需提供名字。// 适用于 C17#include functional#include memory#include string#include unordered_mapclass Base {public:virtual ~Base() default;virtual std::string describe() const 0;};class Registry {public:using Factory std::functionstd::unique_ptrBase();static Registry instance() {static Registry registry;return registry;}void add(const std::string name, Factory factory) {factories_[name] std::move(factory);}std::unique_ptrBase create(const std::string name) const {const auto it factories_.find(name);if (it factories_.end()) return nullptr;return it-second();}private:std::unordered_mapstd::string, Factory factories_;};template class Tstruct Registrar {explicit Registrar(const std::string name) {Registry::instance().add(name, [] { return std::make_uniqueT(); });}};#define REGISTER_CLASS(T) \[[maybe_unused]] static RegistrarT g_reg_##T(#T)使用时在每个具体类的实现文件里写一行REGISTER_CLASS(Circle);。宏展开成一个静态对象它的构造函数在main之前执行把Circle和创建函数登记进去。#T把标识符转成字符串g_reg_##T用记号连接生成唯一变量名。// 适用于 C17class Circle : public Base {public:std::string describe() const override { return circle; }};REGISTER_CLASS(Circle);调用方// 适用于 C17std::unique_ptrBase make(const std::string name) {return Registry::instance().create(name);}这套机制有三个必须注意的工程点。第一静态对象的初始化顺序跨翻译单元未指定但注册只依赖Registry::instance()里的函数内静态对象首次调用时构造因此不受顺序影响。第二静态库链接器可能把「没有被任何符号引用」的目标文件整个丢掉注册代码也就不会执行需要显式引用、使用 whole-archive 链接选项或把注册放进一定会被链接的源文件。第三注册表在main之前构建多线程环境下如果运行期还要动态注册必须加锁。三、属性反射手动登记字段访问器比「按名字创建」更进一步的是「按名字读写字段」。标准 C 无法自动枚举成员只能为每个字段手工登记一个 getter 和一个 setter通常存成std::function。// 适用于 C17#include functional#include stdexcept#include string#include unordered_mapclass Object {public:virtual ~Object() default;};class Meta {public:using Getter std::functionstd::string(const Object);using Setter std::functionvoid(Object, const std::string);void add(const std::string name, Getter getter, Setter setter) {fields_[name] Field{std::move(getter), std::move(setter)};}std::string get(const Object obj, const std::string name) const {const auto it fields_.find(name);if (it fields_.end())throw std::out_of_range(unknown field: name);return it-second.getter(obj);}void set(Object obj, const std::string name, const std::string value) const {const auto it fields_.find(name);if (it fields_.end())throw std::out_of_range(unknown field: name);it-second.setter(obj, value);}private:struct Field {Getter getter;Setter setter;};std::unordered_mapstd::string, Field fields_;};为某个类登记字段时getter 和 setter 里必须把Object转回具体类型。这个转换的正确性由登记代码保证只有该类型的对象才会用这套字段表。如果把它做成「所有类型共用一张表」就必须在表里记录类型标识并在访问前校验否则static_cast到错误类型是 UB。属性表常见的用途是序列化、配置绑定、编辑器检视面板。它的优点是类型安全边界清晰每个字段的转换逻辑由写登记代码的人负责缺点是完全手动新增字段必须记得登记漏了不会编译报错只会在运行时表现为「字段不见了」。代码生成工具解析头文件或 IDL 生成注册代码能缓解这个问题但引入了构建步骤。四、边界与替代方案能力标准 C手动注册方案按名字创建对象不支持支持工厂表枚举成员不支持需手动登记按名字读写字段不支持支持访问器表按名字调用任意函数不支持需为每个函数写包装获取可读类名typeid().name()为实现定义注册时自己给名字跨语言/跨进程不支持需自定义协议如果项目能接受额外工具可以用 IDL、注解式宏或外部代码生成器来减少样板如果只是少量类型需要按名字创建手写注册表最直接。不要把注册表当成「完整反射」——它只覆盖你显式登记过的东西。常见坑点❌ 用typeid(obj).name()的结果当类名去查表或显示给用户。✅ 注册时自己指定稳定名字name()是实现定义的修饰名。❌ 把REGISTER_CLASS写在头文件里被多个源文件包含。✅ 放在具体类的实现文件中避免重复注册与无谓的目标文件膨胀。❌ 注册代码放在静态库里链接器没有拉取该目标文件注册未执行。✅ 显式引用该目标文件或使用 whole-archive 链接选项。❌ 认为dynamic_cast能按字符串创建对象。✅ 它只能在已知目标类型时做转换按名字创建需要工厂表。❌ 在运行期多线程动态注册却不加锁。✅ 注册集中在启动阶段或给注册表加互斥量。❌ 属性访问器里static_cast到错误的具体类型。✅ 每个类型独立字段表或访问前校验动态类型。❌ 以为 C26 反射已经可以直接使用。✅ 截至本文写作时标准尚未发布该能力按项目工具链实际情况确认。总结层面能做什么不能做什么RTTI识别动态类型、安全向下转换枚举成员、按名创建工厂注册表按名字创建对象自动发现未注册类型属性表按名字读写字段自动生成访问器代码生成减少手写登记需要额外构建步骤C 没有内建反射这不是缺陷而是「零开销」取向的取舍运行期类型信息只保留到能支持虚调用和安全转换的程度。工程上的反射都是显式注册出来的它的可靠性取决于登记是否完整。把注册集中在启动阶段、把名字与创建函数绑定、把字段访问器的类型转换封装在类型内部就能用很小的代价获得「够用」的反射能力。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑