# 反向改写：把 Rust 转回 C++

## Part 1：方法与第一例子 —— hexyl

*Su Qingyue*

---

### 起点

这些年，系统编程里很常见的一句话是：“重写成 Rust 吧。”

有时这句话说的是安全，有时说的是“表达力更强”。这篇系列文章想问的，其实是一个更窄、也更容易落地的问题：

> 如果把一个写得不错的 Rust 程序，反过来转写成地道的现代 C++，到底会留下什么，又会丢掉什么？

如果 Rust 的优势主要来自表达力，那么反向转写很快就应该撞上“C++ 说不出来”的东西。反过来，如果大部分翻译都还是局部、机械、可复现的，那么真正更大的差别，可能不在“能不能表达”，而在“编译器能不能拒绝不安全的写法”。

我想把这两件事分开谈。表达力和拒绝能力当然有关，但它们不是一回事。把它们混成一句“Rust 更好”，讨论往往就会变得含糊。

### 选题：hexyl

[hexyl](https://github.com/sharkdp/hexyl) 是 David Peter（sharkdp）写的一个命令行十六进制查看器。规模大概 2400 行 Rust，不算大，但已经足够完整，能覆盖一批很典型的 Rust 习惯：

- 带数据的 `enum`
- trait 实现
- 泛型
- builder 风格 API
- `const fn`
- `LazyLock`
- `anyhow` / `thiserror` 风格的错误处理
- `clap` derive 命令行定义
- 带 guard 的模式匹配

也就是说，它不是玩具程序，但也还没有大到无法完整转写。

### 方法

这里的规则很简单：

**目标是写出“像 C++ 程序员会写的 C++23”，而不是把 Rust 机械地换一层语法皮。**

流程分成四步：

1. 先看清 Rust 项目的模块结构、依赖和主要特性。
2. 按模块转写，从内部依赖最少的部分开始。
3. 用行为测试核对结果，尽量做到输出逐字节一致。
4. 把每一处关键映射都记下来。

### 主要映射

| Rust | C++ | 说明 |
|------|-----|------|
| 简单 `enum` | `enum class` | 基本直接对应 |
| 带数据的 `enum` | `std::variant` 或 tagged struct | 例如 `IncludeMode::File(String)` |
| `match` | `switch` + `-Wswitch-enum` | 用编译器警告补一点覆盖性检查 |
| `Option<T>` | `std::optional<T>` | 语义几乎一致 |
| `Result<T, E>` | `std::expected<T, E>` | C++23 对应物 |
| trait 实现 | 类方法 / 接口 | 取决于具体用途 |
| 泛型 writer | `std::ostream&` | C++ 里已经有统一写接口 |
| builder（消耗 `self`） | 返回引用的 builder | 更接近 C++ 习惯 |
| `LazyLock<T>` | 函数内 `static` | C++11 起线程安全 |
| `const fn` | `constexpr` | 这里是自然映射 |
| `anyhow::Result` | 异常 | CLI 工具里更自然 |
| `thiserror` derive | 手写错误信息 | 规模不大时够用 |
| `Box<dyn Read>` | `std::unique_ptr<std::istream>` | trait object 对到虚接口 |
| `Vec<T>` | `std::vector<T>` | 直接映射 |
| `String` / `&str` | `std::string` / `std::string_view` | 直接映射 |

### 转过去以后，什么消失了

有些 Rust 特性在 C++ 里不是“写不出来”，而是“写得出来，但编译器不替你担保”。

**生命周期。** Rust 里的 `Printer<'a, Writer: Write>` 会把 printer 和 writer 的生命周期关系写在类型里。C++ 里只剩一个 `std::ostream&`。关系还在，但约束不再是编译器的责任。

**借用检查。** Rust 的 `&mut self` 保证独占访问。C++ 的 `this` 没有等价的静态验证。

**`pub(crate)` 这种模块边界。** 到 C++ 里，多半只剩头文件和约定。

**`#[non_exhaustive]` 一类提示。** 行为本身不变，但类型层面的承诺没了。

这里真正消失的不是程序逻辑，而是证明义务被谁承担。

### 哪些地方需要额外小心

不是所有东西都能一眼一对一。

**字节分类。** Rust 的 `u8::is_ascii_whitespace()` 和 C 里的 `isspace()` 细节并不完全一样，尤其 `\x0B` 这种边角字节。这个地方如果偷懒，输出会变。

**UTF-8 输出。** Rust 的 `char` 天然就是 Unicode 标量值；C++ 的 `char` 只是字节。遇到 box-drawing 字符、braille 字符和一些特殊符号时，必须显式编码成 UTF-8。

**整数溢出。** Rust debug 模式会 panic，C++ 的 signed overflow 是未定义行为。hexyl 这个例子里没踩到，但它是反向转写时必须记在脑子里的系统性差异。

### 结果

| 指标 | Rust | C++ |
|------|------|-----|
| 源码行数（库 + CLI） | 2,392 | 2,111 |
| 外部依赖 | 8 个 crate | 1 个 CLI 库 |
| 构建系统 | Cargo | CMake |
| C++ 标准 | — | C++23 |
| 行为差异 | — | 未观察到，输出一致 |

C++ 版本反而更短，大约少了 12%。

这不代表“C++ 天生更简洁”，更像是这几个局部因素叠在一起：

1. `std::ostream` 让一层泛型消失了。
2. CLI11 的 API 在这里比 clap derive 更短。
3. Rust 把测试模块内嵌在源码里，C++ 测试是分文件放的。

### 这个例子到底说明了什么

对 hexyl 这样的项目，我觉得它支持的是一个很窄的结论：

**这次遇到的 Rust 构造，基本都能在 C++ 里找到自然对应物；翻译过程大体是机械的，而不是重新发明。**

但它**没有**说明：

- C++ 版本和 Rust 一样安全；
- Rust 的保证可以自动跟着代码结构一起搬过去；
- 只要能转写成功，就可以忽略编译器验证的价值。

更接近事实的说法是：

在这个例子里，程序结构大体能迁移，行为也能核对；真正没有一起迁移过去的，是 Rust 编译器替程序员承担的那部分验证。

### 下一篇

第二篇会把问题拉得更完整一点：不是只看一个 Rust 项目，而是看 Rust 版 coreutils 转回 C++ 之后，再和更早的 GNU C 版本一起对照。三种语言，同一个工具集，能更清楚地看到什么变了，什么其实没变。

---

*这次转写对应的完整材料放在 [`01-hexyl`](../01-hexyl/)。C++ 版本通过了 26 个行为测试，输出与原 Rust 程序逐字节一致。*

**关于作者。** Su Qingyue 负责这个实验的选题、方法和最后的文字整理。实际转写、草稿撰写和材料归档都借助了 AI 工作流，再由我回头收束和修订。
