☰
C++ 反射机制详解及实例代码
2026/11/4 7:46:30 网站建设 项目流程

前言


反射(reflection)指程序在运行时检查自身的类型、成员、函数,并据此创建对象或调用方法。Java、C# 有完整的反射;标准 C++ 至今没有。C++ 只有一套很有限的运行期类型信息(run-time type information, RTTI),能做类型识别和安全的向下转换,但不能枚举一个类有哪些成员、不能按名字调用任意函数、不能根据字符串创建任意类型。


常见的误解有三个。第一,把typeid当成反射,以为typeid(obj).name()能拿到可用的类名并据此创建对象——它返回的往往是编译器修饰过的名字,且标准不保证内容。第二,以为dynamic_cast能替代反射,实际上它只能在已知目标类型的前提下做转换,不能从字符串反推类型。第三,以为 C++26 的反射提案已经是可用标准,实际上截至本文写作时它尚未成为已发布的正式标准,各编译器的支持情况需要单独确认。


工程上所谓的「C++ 反射」,通常指手动注册 + 查表:在程序启动时把「名字 → 创建函数」「名字 → 字段访问器」登记到一张表里,运行时按名字查表。本文给出这套做法的完整示例,并说明它的边界。


一、RTTI 能提供什么


RTTI 的两个主要工具是typeid和dynamic_cast。typeid作用于多态类型的左值时,返回动态类型的std::type_info;作用于非多态表达式时,返回静态类型。type_info::name()返回的字符串由实现决定,常见的是经过名称修饰(name mangling)的形式,不能直接当类名用。type_info支持==比较,也可以用来做类型索引。


// 适用于 C++17
#include <iostream>
#include <typeinfo>

struct Base {
virtual ~Base() = default;
};
struct Derived : Base {};

int main() {
Derived d;
Base& b = d;

std::cout << typeid(b).name() << '\n'; // 动态类型 Derived
std::cout << typeid(Base).name() << '\n'; // 静态类型 Base

if (dynamic_cast<Derived*>(&b) != nullptr)
std::cout << "b really refers to Derived\n";
}

dynamic_cast<Derived*>(p)在p为空时返回空指针;转换失败也返回空指针。转换成引用形式dynamic_cast<Derived&>(r)失败时抛std::bad_cast。它要求源类型是多态类型(至少有一个虚函数),否则编译不通过。


RTTI 可以被编译器选项关闭(例如 GCC / Clang 的-fno-rtti),关闭后typeid和dynamic_cast对多态类型不可用。一些性能敏感或嵌入式项目会这样做,这也是「不能依赖 RTTI 做核心架构」的原因之一。


二、注册表:用字符串创建对象


最常用的「反射」是工厂注册:每个类在启动时把自己的名字和一个创建函数登记到全局表,调用方只需提供名字。


// 适用于 C++17
#include <functional>
#include <memory>
#include <string>
#include <unordered_map>

class Base {
public:
virtual ~Base() = default;
virtual std::string describe() const = 0;
};

class Registry {
public:
using Factory = std::function<std::unique_ptr<Base>()>;

static Registry& instance() {
static Registry registry;
return registry;
}

void add(const std::string& name, Factory factory) {
factories_[name] = std::move(factory);
}

std::unique_ptr<Base> create(const std::string& name) const {
const auto it = factories_.find(name);
if (it == factories_.end()) return nullptr;
return it->second();
}

private:
std::unordered_map<std::string, Factory> factories_;
};

template <class T>
struct Registrar {
explicit Registrar(const std::string& name) {
Registry::instance().add(name, [] { return std::make_unique<T>(); });
}
};

#define REGISTER_CLASS(T) \
[[maybe_unused]] static Registrar<T> g_reg_##T(#T)

使用时,在每个具体类的实现文件里写一行REGISTER_CLASS(Circle);。宏展开成一个静态对象,它的构造函数在main之前执行,把Circle和创建函数登记进去。#T把标识符转成字符串,g_reg_##T用记号连接生成唯一变量名。


// 适用于 C++17
class Circle : public Base {
public:
std::string describe() const override { return "circle"; }
};

REGISTER_CLASS(Circle);

调用方:


// 适用于 C++17
std::unique_ptr<Base> make(const std::string& name) {
return Registry::instance().create(name);
}

这套机制有三个必须注意的工程点。第一,静态对象的初始化顺序跨翻译单元未指定,但注册只依赖Registry::instance()里的函数内静态对象,首次调用时构造,因此不受顺序影响。第二,静态库链接器可能把「没有被任何符号引用」的目标文件整个丢掉,注册代码也就不会执行;需要显式引用、使用 whole-archive 链接选项,或把注册放进一定会被链接的源文件。第三,注册表在main之前构建,多线程环境下如果运行期还要动态注册,必须加锁。


三、属性反射:手动登记字段访问器


比「按名字创建」更进一步的是「按名字读写字段」。标准 C++ 无法自动枚举成员,只能为每个字段手工登记一个 getter 和一个 setter,通常存成std::function。


// 适用于 C++17
#include <functional>
#include <stdexcept>
#include <string>
#include <unordered_map>

class Object {
public:
virtual ~Object() = default;
};

class Meta {
public:
using Getter = std::function<std::string(const Object&)>;
using Setter = std::function<void(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_map<std::string, Field> fields_;
};

为某个类登记字段时,getter 和 setter 里必须把Object&转回具体类型。这个转换的正确性由登记代码保证:只有该类型的对象才会用这套字段表。如果把它做成「所有类型共用一张表」,就必须在表里记录类型标识并在访问前校验,否则static_cast到错误类型是 UB。


属性表常见的用途是序列化、配置绑定、编辑器检视面板。它的优点是类型安全边界清晰(每个字段的转换逻辑由写登记代码的人负责),缺点是完全手动:新增字段必须记得登记,漏了不会编译报错,只会在运行时表现为「字段不见了」。代码生成工具(解析头文件或 IDL 生成注册代码)能缓解这个问题,但引入了构建步骤。


四、边界与替代方案




能力标准 C++手动注册方案



按名字创建对象不支持支持(工厂表)

枚举成员不支持需手动登记

按名字读写字段不支持支持(访问器表)

按名字调用任意函数不支持需为每个函数写包装

获取可读类名typeid().name()为实现定义注册时自己给名字

跨语言/跨进程不支持需自定义协议



如果项目能接受额外工具,可以用 IDL、注解式宏或外部代码生成器来减少样板;如果只是少量类型需要按名字创建,手写注册表最直接。不要把注册表当成「完整反射」——它只覆盖你显式登记过的东西。


常见坑点



  1. ❌ 用typeid(obj).name()的结果当类名去查表或显示给用户。


✅ 注册时自己指定稳定名字;name()是实现定义的修饰名。



  1. ❌ 把REGISTER_CLASS写在头文件里,被多个源文件包含。


✅ 放在具体类的实现文件中,避免重复注册与无谓的目标文件膨胀。



  1. ❌ 注册代码放在静态库里,链接器没有拉取该目标文件,注册未执行。


✅ 显式引用该目标文件,或使用 whole-archive 链接选项。



  1. ❌ 认为dynamic_cast能按字符串创建对象。


✅ 它只能在已知目标类型时做转换;按名字创建需要工厂表。



  1. ❌ 在运行期多线程动态注册,却不加锁。


✅ 注册集中在启动阶段,或给注册表加互斥量。



  1. ❌ 属性访问器里static_cast到错误的具体类型。


✅ 每个类型独立字段表,或访问前校验动态类型。



  1. ❌ 以为 C++26 反射已经可以直接使用。


✅ 截至本文写作时标准尚未发布该能力,按项目工具链实际情况确认。


总结




层面能做什么不能做什么



RTTI识别动态类型、安全向下转换枚举成员、按名创建

工厂注册表按名字创建对象自动发现未注册类型

属性表按名字读写字段自动生成访问器

代码生成减少手写登记需要额外构建步骤



C++ 没有内建反射,这不是缺陷,而是「零开销」取向的取舍:运行期类型信息只保留到能支持虚调用和安全转换的程度。工程上的反射都是显式注册出来的,它的可靠性取决于登记是否完整。把注册集中在启动阶段、把名字与创建函数绑定、把字段访问器的类型转换封装在类型内部,就能用很小的代价获得「够用」的反射能力。




需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询