C++ 为什么不行:名字在编译后就没了 · znzryb
## 名字在编译后就没了
把上一节那个实验原样搬到 C++:
```cpp
#include <cstdio>
struct Secret { int zebraField; int giraffeField; };
int main() { Secret s{1, 2}; std::printf("%d %d\n", s.zebraField, s.giraffeField); }
```
```bash
clang++ -O2 names.cpp -o names && strings names | grep -c -E 'zebraField|giraffeField'
```
```text
0
```
一个都没有。C++ 编译器看到 `s.zebraField`,直接换成「从 `s` 开头往后数 0 个字节」;看到 `s.giraffeField`,换成「往后数 4 个字节」。**名字只活在编译期间,程序编译完就只剩下偏移量**。(加 `-g` 的调试信息里会有名字,但那是给调试器看的,程序自己读不到。)
这是 C++ 有意的选择,叫**零开销原则**:你没用到的功能,不该为它付出代价。绝大多数程序从来不需要在运行时查字段名,那就不该给每个程序都塞一份说明书。
所以 `std::println` 拿到一个 `P` 的时候,手里只有一块内存,它不知道这块内存该怎么切、每块叫什么。只能你来告诉它。
## 老办法一:手写 `formatter` 特化
这就是报错让我们干的事:
```cpp
#include <format>
#include <print>
#include <string>
struct P {
int a;
std::string b;
double c;
};
// 字段名是我们自己敲进字符串里的
template <>
struct std::formatter<P> {
constexpr auto parse(std::format_parse_context& ctx) { return ctx.begin(); }
auto format(const P& p, std::format_context& ctx) const {
return std::format_to(ctx.out(), "{{a:{} b:{} c:{}}}", p.a, p.b, p.c);
}
};
int main() {
P p{1, "hi", 2.5};
std::println("{}", p);
}
```
```text
{a:1 b:hi c:2.5}
```
两个函数:`parse` 处理 `{}` 里的格式选项(这里不支持选项,直接返回),`format` 负责真正输出。格式串里的 `{{` 和 `}}` 是转义,输出成一个 `{` 和一个 `}`。
能用,但字段名 `a`、`b`、`c` 是**我们手抄进字符串的**。每个结构体都要写一遍。
## 老办法二:宏
宏能做一件函数做不到的事:`#x` 能把参数**原样变成字符串**。写 `FIELD(a)`,`#x` 就是 `"a"`,`obj.x` 就是 `obj.a` —— 名字和值一次都拿到了。
```cpp
#include <format>
#include <print>
#include <string>
// 字段名用 #x 变成字符串,值用 obj.x 取出来
#define FIELD(x) std::format_to(out, "{}{}:{}", sep, #x, obj.x), sep = " "
#define REFLECT3(T, f1, f2, f3) \
template <> \
struct std::formatter<T> { \
constexpr auto parse(std::format_parse_context& c) { return c.begin(); } \
auto format(const T& obj, std::format_context& ctx) const { \
auto out = ctx.out(); \
const char* sep = ""; \
*out++ = '{'; \
FIELD(f1); FIELD(f2); FIELD(f3); \
*out++ = '}'; \
return out; \
} \
};
struct P {
int a;
std::string b;
double c;
};
REFLECT3(P, a, b, c) // 字段又列了一遍
int main() {
std::println("{}", P{1, "hi", 2.5});
}
```
```text
{a:1 b:hi c:2.5}
```
`formatter` 不用每个类型手写了,但**字段还是要再列一遍**:`REFLECT3(P, a, b, c)`。真正的库(比如 Boost.Describe 的 `BOOST_DESCRIBE_STRUCT`、nlohmann/json 的 `NLOHMANN_DEFINE_TYPE_INTRUSIVE`)会用宏技巧支持任意个字段,但本质一样:**你把字段名再说一遍,宏替你把它变成字符串**。
## 老办法三:结构化绑定(Boost.PFR 的思路)
有没有办法连字段都不用列?C++17 的结构化绑定 `auto& [x, y, z] = t;` 能把一个结构体拆成几个变量。只要知道它有几个字段,就能拆:
```cpp
#include <print>
#include <string>
struct Any {
template <class T> operator T() const; // 能冒充任何类型
};
template <class T>
consteval int field_count() { // 试着用 4、3、2…… 个 Any 去初始化 T
if constexpr (requires { T{Any{}, Any{}, Any{}, Any{}}; }) return 4;
else if constexpr (requires { T{Any{}, Any{}, Any{}}; }) return 3;
else if constexpr (requires { T{Any{}, Any{}}; }) return 2;
else return 1;
}
template <class T>
void print_values(const T& t) {
if constexpr (field_count<T>() == 3) {
const auto& [x, y, z] = t;
std::println("{{{} {} {}}}", x, y, z); // 只有值,没有名字
} else if constexpr (field_count<T>() == 2) {
const auto& [x, y] = t;
std::println("{{{} {}}}", x, y);
}
}
struct P { int a; std::string b; double c; };
struct Q { int x; int y; };
int main() {
print_values(P{1, "hi", 2.5});
print_values(Q{3, 4});
}
```
```text
{1 hi 2.5}
{3 4}
```
`field_count` 的把戏:`Any` 能冒充任何类型,于是拿 3 个 `Any` 去初始化 `P` 能成功、拿 4 个就失败,由此数出 `P` 有 3 个字段。
`P` 和 `Q` 都没为打印写过任何代码,值确实拆出来了。但**名字拆不出来** —— 拆出来的变量叫 `x`、`y`、`z`,那是我们起的名字,不是结构体里的。效果相当于 Go 的 `%v`,够不着 `%+v`。
Boost.PFR 后来还靠编译器的 `__PRETTY_FUNCTION__`(一个写着当前函数完整签名的字符串)把字段名硬抠了出来。这能用,但已经是在钻编译器的空子了。
## 三条老办法的共同毛病
| 办法 | 字段名从哪来 | 加一个字段时 |
|---|---|---|
| 手写 `formatter` | 你手抄进字符串 | 要改 `formatter`,**忘了改不报错**,只是少打一个字段 |
| 宏 | 你在宏里再列一遍 | 要改宏调用,忘了改同样不报错 |
| 结构化绑定 | 拿不到 | 不用改,但只有值没有名字 |
根子都在一个地方:**C++ 不允许程序查看自己的类型**。下一节,C++26 终于把这面镜子装上了。