---
series:
  - Reflection
series_order: 1
title: 写给 C++ 程序员的反射教程
date: "2023-11-29 09:14:02"
updated: "2026-03-29 15:09:40"
zhihu_article_id: "669358870"
zhihu_url: https://zhuanlan.zhihu.com/p/669358870
zhihu_column_id: c_1707545619290316800
zhihu_column_title: 编程语言中的反射
---

## What is Reflection?

反射 (Reflection) 这个词相信大家都不陌生了，也许你没用过但是你一定听过。然而，就像 CS 领域很多其他的**惯用词**一样，对于反射，并没有一个清晰而准确的定义。于是就会出现这种情况：对于 C#, Java, Python 这些拥有反射的语言，谈论到反射可以很自然的联想到对应语言中相关的设施，API 和代码示例，非常的具体。而对于 C, C++, Rust 这些没有反射的语言，当谈论起反射的时候，大家都不确定对方指的是什么，非常的不具体。比如有人告诉我说 Rust 有反射，他给出的例子是 Rust 的官方的文档中对 [std::Any 模块](https://doc.rust-lang.org/stable/std/any/index.html) 的介绍。里面提到了

> Utilities for dynamic typing or type reflection 用于动态类型或类型反射的工具

但是尴尬就尴尬在，你说它是反射吧，功能非常鸡肋，你说它不是吧，硬要说有这种体现也不是不行。

类似的情况在 C++ 中也经常发生。相信你也经常能听到如下观点：C++ 只有非常弱的反射即 RTTI(Run Time Type Information)，但是 C++ 的一些框架比如 QT，UE 自己实现了反射。在最近的讨论中，网上的博客中又或者 C++ 新标准的提案中，你可能又会听到所谓：

- 静态反射 (static reflection)
- 动态反射 (dynamic reflection)
- 编译期反射 (compile time reflection)
- 运行期反射 (runtime reflection)

这样一大堆名词实在是让人听得云里雾里，晕头转向。而且 static, dynamic, compile time, runtime 这些前缀词本身也都是惯用词，经常和各种词组合起来，根据语境不同有非常多的含义。

有的读者可能会说，我查了 WIKI，[反射](https://en.wikipedia.org/wiki/Reflective_programming) 明明就是有定义的啊，如下：

> In computer science, reflective programming or reflection is the ability of a process to examine, introspect, and modify its own structure and behavior. <br>反射是程序具有自省，检查和修改它自身结构和行为的一种能力。

那首先，WIKI 也是人写的，不具有绝对的权威性，如果你对这个定义不满意，是可以自己修改的。其次这里的用词也是很模糊的，什么叫自省 (introspect)？自我反省，在 CS 中这个词又是什么意思呢？所以这个定义也是很尴尬的。那怎么办呢？我选择把它拆分成几个过程进行解释，这样我们就不用去纠结「**反射究竟是什么**」这个概念问题了。取而代之的是，弄明白了这几个过程，自然而然的你就明白反射是在做什么事情了。

## How to Understand Reflection?

所有语言的反射都可以看成下面这三步：

### Generate Metadata

首先什么是元数据 (Metadata) 呢？我们在写代码的时候都会给变量，类型，结构体字段什么的取名字。这些名字主要是为了方便程序员理解和维护源代码。对于 C/C++ 来说，这些名字在编译之后通常会被丢弃，为了节省二进制空间嘛，可以理解。详细的讨论请见 [为什么说 C/C++编译器不保留元信息](https://www.ykiko.me/zh-cn/articles/670190357)。

但是渐渐地，我们发现某些情况下是需要这些数据的。比如把结构体序列化成 `json` 的时候就需要结构体字段名，在打印日志的时候不希望打印枚举值，而是直接打印对应的枚举名。怎么办呢？早期，只能通过 hard code 的方式，也就是手写，高级点的可能来点宏。这样其实是很不方便的，不利于后续的代码维护。

后来有一些语言，例如 Java 和 C#。他们的编译器在编译的时候会保留包括这些名字在内的很多数据，这些数据就叫做元数据（Metadata）。同时，也有一些手段允许程序员自己附加元数据在某些结构上，例如 C# 的 `attribute`，Java 的 `annotation`。

对于 C++ 来说呢？目前 C++ 编译器只会保留类型名用于实现 RTTI，即标准中 `std::type_info` 的相关设施。其他的信息，编译器都会抹除掉。怎么办呢？手动编写元数据对于少量的类来说还可以接受，但当项目规模增大时，例如有几十或上百个类时，将变得非常繁琐和容易出错。其实，我们可以在实际编译之前，运行一个脚本来负责生成这些数据，也就是所谓的代码生成 (Code Generation)。相关内容请参考 [使用 Clang 工具自由的支配 C++ 代码吧](https://www.ykiko.me/zh-cn/articles/669360731)。

### Query Metadata

生成完之后，接下来就是查询元数据了。很多语言内置的反射模块，例如 Python 的 `inspect`，Java 的 `Reflection`，C# 的 `System.Reflection`，其实就是封装了一些操作，使得用户不用直接接触原始的元数据，用起来更加方便。

值得注意的是，上面这些案例中的查询都是发生在运行时的。在运行时根据字符串进行搜索和匹配，这其实是一个比较慢的过程，所以我们常说反射调用方法比正常调用方法慢。

对于 C++ 来说，编译器提供了一些有限的接口让我们在编译时访问（反射）一些信息，例如使用 `decltype` 可以获取一个变量的类型，进一步还能判断两个变量类型是否相等，是否是某个类型的子类等等，但是功能十分有限。

不过，可以按照上一小节的方法自己生成元信息，把它们都标记为 constexpr，然后就可以在编译期进行查询。事实上 C++26 的静态反射也就是这个思路，由编译器生成元信息，暴露给用户一些接口进行查询。相关内容请见 [C++26 静态反射提案解析](https://www.ykiko.me/zh-cn/articles/661692275)。而查询的时机也就是所谓的**动态反射**和**静态反射**的区别。

当然了，编译期可做的事情肯定是没有运行期那么多的，例如希望根据运行时的类型名创建类实例，无论如何编译期肯定没法做到。但你可以基于这些静态的元信息构建动态反射。相关内容请见 [在 C++ 中实现 Object](https://www.ykiko.me/zh-cn/articles/670191053)。

### Operate Metadata

然后，就是根据元数据进行进一步的操作，比如代码生成。这个在 C++ 中可以理解为编译期的代码生成，在 Java 和 C# 则可以认为是运行期的代码生成。详见 [各种姿势进行代码生成](https://www.ykiko.me/zh-cn/articles/669359855) 。

## Conclusion

最后，我们来用上面三个步骤分解一下不同语言中的反射：

- Python, JavaScript, Java, C#：由编译器/解释器生成元数据，标准库提供接口，用户可以在运行时查询元数据，同时由于有虚拟机（VM），可以方便地生成代码。
- Go：由编译器生成元数据，标准库提供接口，用户可以在运行时查询元数据。但是由于 Go 主要是 AOT（Ahead-of-Time）编译，运行时生成代码并不方便。
- Zig, C++26 静态反射：由编译器生成元数据，标准库提供接口，用户可以在编译时查询元数据。同样由于是 AOT 编译，运行时生成代码并不方便，但是可以在编译期进行代码生成。<br>

而 QT 和 UE 则是通过代码生成，自己生成了元数据，封装了接口，用户可以在运行时查询元数据。实现原理上类似 Go 的反射。

希望这个系列教程对你有所帮助！如果有错误欢迎评论区讨论，感谢你的阅读。
