一、C++为什么没有反射?这不是偷懒,是设计哲学

反射,说白了就是程序在运行时能「看穿」自己的结构——知道有哪些类、哪些方法、哪些成员变量。Java、C# 都有,但 C++ 就是不给。

为什么?我早年也困惑过。后来做底层系统开发,慢慢就理解了。

C++ 的设计目标有一条铁律:不为不需要的东西付出代价。反射需要维护大量的元数据——类名、方法签名、继承关系、成员偏移量……这些信息在编译后通常可以扔掉。如果每个类都背着这些信息,内存占用会暴涨,运行时性能也会下降。

你想想看,嵌入式系统、游戏引擎、高频交易,哪个能容忍这种开销?

另外,C++ 的编译模型是「独立编译单元 + 链接」。每个 .cpp 文件单独编译,最后链接到一起。反射需要全局的类型信息,这跟 C++ 的编译模型天然冲突。要实现反射,要么改编译器的架构,要么引入额外的元数据生成步骤——嗯,Qt 的 MOC 就是这么干的。

核心结论:C++ 不内置反射,不是做不到,而是不想为所有用户强加这个成本。你要反射?自己造,或者用工具生成。

二、利用虚函数模拟反射——我项目里的实战方案

虽然没有原生反射,但虚函数 + 一些巧妙的架构设计,可以模拟出反射的核心能力。我在做插件系统时就用过这套方案。

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)。它主要靠两个东西:typeiddynamic_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 信息就存不进去,编译直接报错。

注意:RTTI 不是免费的。每个带虚函数的类都会额外存储 type_info 指针。如果内存极度敏感(比如嵌入式),可以用 -fno-rtti 关掉它。但关掉后 dynamic_cast 和 typeid 都不能用了。

四、RTTI 与反射的差距——一张表说清楚

能力 Java 反射 C++ RTTI 虚函数模拟
获取类名 ✅ 完整类名 ⚠️ 编译器相关 ✅ 自定义
获取方法列表 ✅ 全部方法 ❌ 不支持 ✅ 需手动注册
动态调用方法 ✅ 按名称调用 ❌ 不支持 ✅ 需手动映射
获取成员变量 ✅ 全部字段 ❌ 不支持 ❌ 需额外实现
运行时开销 较高 中等
编译时开销 无(手动注册)

看到差距了吧?C++ 的 RTTI 只能告诉你「我是谁」,不能告诉你「我有什么」。真正的反射需要的是后者。

五、知识体系总览

下面这张图是我梳理的本章核心逻辑。从 C++ 为什么没有反射,到如何用虚函数模拟,再到 RTTI 的定位,一目了然。

虚函数与反射:知识体系 C++ 反射机制 为什么没有反射? 设计哲学:不为不需要的付费 虚函数模拟反射 手动注册 + 宏简化 RTTI 运行时类型信息 typeid / dynamic_cast 基类 IReflectable 接口 派生类实现虚函数 宏批量注册方法 typeid 获取类型名称 dynamic_cast 安全向下转换 总结:C++ 无原生反射 → 虚函数模拟弥补 → RTTI 提供有限支持

六、实际项目中的取舍建议

做了这么多年 C++,我总结了几条经验:

  • 小项目(< 50 个类):手写虚函数模拟反射,简单可控,没有外部依赖。
  • 中型项目(50-200 个类):用宏 + 模板减少重复代码,或者考虑 Qt MOC 这类代码生成器。
  • 大型项目(200+ 个类):认真评估是否需要真正的反射。如果需要,上代码生成工具(如 protobuf、反射框架),别自己造轮子。
  • 性能敏感场景:尽量避开 RTTI 和虚函数模拟。用编译期多态(模板、CRTP)替代。
我的习惯:在项目初期就定好反射策略。别等到代码写了一半,发现需要序列化或插件加载,再回头加反射——那时候改起来就痛苦了。

嗯,关于虚函数与反射,今天就聊到这儿。记住一句话:C++ 不给你的,你可以自己造,但要清楚代价是什么。

公众号:蓝海资料掘金营,微信deep3321