一、C++为什么没有反射?这不是偷懒,是设计哲学
反射,说白了就是程序在运行时能「看穿」自己的结构——知道有哪些类、哪些方法、哪些成员变量。Java、C# 都有,但 C++ 就是不给。
为什么?我早年也困惑过。后来做底层系统开发,慢慢就理解了。
C++ 的设计目标有一条铁律:不为不需要的东西付出代价。反射需要维护大量的元数据——类名、方法签名、继承关系、成员偏移量……这些信息在编译后通常可以扔掉。如果每个类都背着这些信息,内存占用会暴涨,运行时性能也会下降。
你想想看,嵌入式系统、游戏引擎、高频交易,哪个能容忍这种开销?
另外,C++ 的编译模型是「独立编译单元 + 链接」。每个 .cpp 文件单独编译,最后链接到一起。反射需要全局的类型信息,这跟 C++ 的编译模型天然冲突。要实现反射,要么改编译器的架构,要么引入额外的元数据生成步骤——嗯,Qt 的 MOC 就是这么干的。
二、利用虚函数模拟反射——我项目里的实战方案
虽然没有原生反射,但虚函数 + 一些巧妙的架构设计,可以模拟出反射的核心能力。我在做插件系统时就用过这套方案。
2.1 基本思路:虚函数作为类型标识
每个类暴露一个虚函数,返回自己的类型信息。基类定义接口,派生类实现。
class IReflectable {
public:
virtual ~IReflectable() = default;
virtual std::string className() const = 0;
virtual std::vector<std::string> methodNames() const = 0;
virtual void invoke(const std::string& method) = 0;
};
class MyClass : public IReflectable {
public:
std::string className() const override { return "MyClass"; }
std::vector<std::string> methodNames() const override {
return {"foo", "bar"};
}
void invoke(const std::string& method) override {
if (method == "foo") foo();
else if (method == "bar") bar();
}
void foo() { std::cout << "foo called\n"; }
void bar() { std::cout << "bar called\n"; }
};
这套方案我用了好几年。简单、直接、没有外部依赖。缺点也很明显——每个方法都要手动注册,类多了容易漏。
2.2 进阶:用宏减少重复代码
我曾经在一个 200+ 类的项目里手写 invoke 函数,写到崩溃。后来用宏批量生成,舒服多了。
#define DECLARE_METHOD(name) void name()
#define REGISTER_METHOD(name) if (method == #name) { name(); return; }
class MyService : public IReflectable {
public:
std::string className() const override { return "MyService"; }
std::vector<std::string> methodNames() const override {
return {"start", "stop", "restart"};
}
void invoke(const std::string& method) override {
REGISTER_METHOD(start)
REGISTER_METHOD(stop)
REGISTER_METHOD(restart)
throw std::runtime_error("unknown method");
}
DECLARE_METHOD(start) { /* ... */ }
DECLARE_METHOD(stop) { /* ... */ }
DECLARE_METHOD(restart) { /* ... */ }
};
三、运行时类型信息(RTTI)——C++ 给你的那点「反射」
C++ 其实给了你一点点反射能力,就是 RTTI(Run-Time Type Information)。它主要靠两个东西:typeid 和 dynamic_cast。
3.1 typeid 操作符
返回一个 std::type_info 对象,包含类的名称(实现定义)。
#include <typeinfo>
Base* obj = new Derived();
const std::type_info& info = typeid(*obj);
std::cout << info.name() << std::endl; // 输出可能是 "7Derived"
注意:info.name() 返回的名字是编译器决定的,不同编译器不一样。GCC 会带长度前缀,MSVC 是完整类名。别拿它做序列化或跨平台通信。
3.2 dynamic_cast 与类型安全向下转换
这个大家应该都用过。它能在运行时检查指针/引用是否真的是目标类型。
Base* base = new Derived();
Derived* derived = dynamic_cast<Derived*>(base);
if (derived) {
// 转换成功,安全使用
} else {
// 转换失败,base 不是 Derived 类型
}
我踩过一个坑:dynamic_cast 要求类至少有一个虚函数。没有虚函数表,RTTI 信息就存不进去,编译直接报错。
-fno-rtti 关掉它。但关掉后 dynamic_cast 和 typeid 都不能用了。
四、RTTI 与反射的差距——一张表说清楚
| 能力 | Java 反射 | C++ RTTI | 虚函数模拟 |
|---|---|---|---|
| 获取类名 | ✅ 完整类名 | ⚠️ 编译器相关 | ✅ 自定义 |
| 获取方法列表 | ✅ 全部方法 | ❌ 不支持 | ✅ 需手动注册 |
| 动态调用方法 | ✅ 按名称调用 | ❌ 不支持 | ✅ 需手动映射 |
| 获取成员变量 | ✅ 全部字段 | ❌ 不支持 | ❌ 需额外实现 |
| 运行时开销 | 较高 | 低 | 中等 |
| 编译时开销 | 无 | 无 | 无(手动注册) |
看到差距了吧?C++ 的 RTTI 只能告诉你「我是谁」,不能告诉你「我有什么」。真正的反射需要的是后者。
五、知识体系总览
下面这张图是我梳理的本章核心逻辑。从 C++ 为什么没有反射,到如何用虚函数模拟,再到 RTTI 的定位,一目了然。
六、实际项目中的取舍建议
做了这么多年 C++,我总结了几条经验:
- 小项目(< 50 个类):手写虚函数模拟反射,简单可控,没有外部依赖。
- 中型项目(50-200 个类):用宏 + 模板减少重复代码,或者考虑 Qt MOC 这类代码生成器。
- 大型项目(200+ 个类):认真评估是否需要真正的反射。如果需要,上代码生成工具(如 protobuf、反射框架),别自己造轮子。
- 性能敏感场景:尽量避开 RTTI 和虚函数模拟。用编译期多态(模板、CRTP)替代。
嗯,关于虚函数与反射,今天就聊到这儿。记住一句话:C++ 不给你的,你可以自己造,但要清楚代价是什么。