同一个结构体,两种待遇 · znzryb
这篇文章想用一个小时讲清楚一件事:**反射到底是什么**。不从定义讲起,从一件每天都会碰到的小事讲起 —— 把一个结构体打印出来。
全文五节,每节标了大概要读多久:
| 节 | 内容 | 时间 |
|---|---|---|
| 1 | 同一个结构体,Go 和 C++ 两种待遇 | 8 分钟 |
| 2 | Go 是怎么做到的:运行时反射 | 15 分钟 |
| 3 | C++ 为什么不行,以及三条老办法 | 12 分钟 |
| 4 | C++26 静态反射:在编译期把名字拿回来 | 15 分钟 |
| 5 | 一张表收尾 + 三道小练习 | 10 分钟 |
文中所有代码都真的编译、运行过,贴出来的输出就是实际输出。
## Go:一行就打出来了
```go
package main
import "fmt"
type P struct {
A int
B string
C float64
}
func main() {
p := P{A: 1, B: "hi", C: 2.5}
fmt.Printf("%v\n", p)
fmt.Printf("%+v\n", p)
fmt.Printf("%#v\n", p)
}
```
输出:
```text
{1 hi 2.5}
{A:1 B:hi C:2.5}
main.P{A:1, B:"hi", C:2.5}
```
先纠正一个常见的小误会:**`%v` 只打值,`%+v` 才把字段名带上**,`%#v` 还会再带上类型名 `main.P`,字符串也加上引号 —— 打出来的样子基本就是一段能贴回代码里的 Go 字面量。
重点是:`P` 是我们自己刚定义的类型,`fmt` 包写出来的时候根本不知道世界上会有一个 `P`。可它照样知道 `P` 有三个字段、分别叫 `A`、`B`、`C`、各是什么类型。
## C++23:编译都过不去
C++23 有了 `std::print` / `std::println`,用起来跟 Python 的 f-string 差不多。标准库的东西,连 `vector` 都能直接打:
```cpp
#include <print>
#include <string>
#include <vector>
struct P {
int a;
std::string b;
double c;
};
int main() {
std::vector<int> v{1, 2, 3};
std::println("{}", v); // 标准库容器:可以
P p{1, "hi", 2.5};
std::println("{}", p); // 自定义结构体:编译失败
}
```
把第二个 `println` 留着去编译(clang 22 + libstdc++ 15),报错的第一行是:
```text
error: static assertion failed due to requirement
'is_default_constructible_v<std::formatter<P, char>>':
std::formatter must be specialized for each type being formatted
```
翻译成人话:**「你要打印 `P`,那你得先告诉我 `P` 怎么打 —— 给 `std::formatter<P>` 写一个特化。」**
也就是说,C++ 的 `println` 自己没法知道 `P` 里有 `a`、`b`、`c`。这件事得你来告诉它。
## 问题就出在这里
同样是「把一个没见过的结构体打出来」:
- Go 的 `fmt` 能自己**查**到这个类型长什么样;
- C++ 的 `println` 查不到,只能等你**告诉**它。
**程序在运行(或编译)的时候,能不能回过头来查看自己代码里的类型长什么样 —— 有几个字段、叫什么名字、是什么类型** —— 这种能力就叫**反射**(reflection)。就像照镜子:程序看见了它自己。
下一节看 Go 是怎么照这面镜子的。