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

## Part 4：走一圈再回来 —— fish shell

*Su Qingyue*

---

## 为什么这个例子特别重要

前面三篇，都是把 Rust 项目转成 C++。第四篇换一个角度：看一个真实发生过 **C++ → Rust 重写** 的项目，再把其中一部分从 Rust 转回 C++。

fish shell 很合适。它是一个维护多年的成熟项目，2023 年开始逐步从 C++ 迁到 Rust。最后一个 C++ 版本是 3.7.1，之后才进入 Rust 时代。

这个例子和前几篇不一样的地方在于，它天然带着“往返对照”的意味：

- 更早的人类手写 C++
- 后来的人工 Rust 重写
- 这次从 Rust 反向转回来的 C++

如果来回一趟之后，代码还是大差不差，那说明很多差别更像验证和工程组织层面的变化。  
如果来回一趟之后，结构已经明显不一样了，那就说明重写里混进了真正的设计变更。

## 选了三个模块

| 模块 | 主要内容 | 选它的原因 |
|------|----------|------------|
| `color` | 颜色解析、命名颜色表、24 位颜色到 256 色映射 | 结构和数据都很丰富 |
| `wgetopt` | 宽字符版 GNU getopt | 这个模块本身就有 GNU C → C++ → Rust 的谱系 |
| `timer` | 资源占用和耗时统计 | 最接近“纯算法控制组” |

## 数字先摆出来

| 模块 | 原始 C++ | Rust | 转写后的 C++ |
|------|---------:|-----:|-------------:|
| color | 493 | 518 | 448 |
| wgetopt | 603 | 578 | 412 |
| timer | 237 | 235 | 227 |
| **总计** | **1,333** | **1,331** | **1,087** |

这里第一眼就能看到两件事：

**第一，fish 的 Rust 和原始 C++ 在这三个模块里几乎一样长。**  
这和前几篇不同。前几篇是从 Rust 转回 C++ 后，C++ 明显更短；fish 这里，原作者写的 Rust 本身就已经很克制。

**第二，转回来的 C++ 还是更短。**  
但这次更短的原因，已经不只是“把 Rust 的类型层验证结构去掉”。因为原始 C++ 本来就没有那套结构。这里减少的很多东西，更像是在利用现代 C++ 标准库把旧代码顺手收掉了。

## 往返之后，有没有收敛

### 命名颜色表：几乎完全收敛

`color` 模块里有一张固定、排序好的命名颜色表。这个东西在三份版本里几乎是一模一样的：

- 名字一样
- 顺序一样
- palette index 一样
- RGB 值一样

连搜索这张表的二分查找逻辑，本质上也没变。

这说明一个很朴素的事实：**数据本身不会因为换语言就突然改写。**

### `exchange`：不是发散，而是趁机变好了

`wgetopt` 里有个 `exchange` 函数，用来调整参数排列，把 option 放到一起。原始 C++ 版本沿用 GNU getopt 的老实现，是一段几十行的交换循环；Rust 重写直接换成了 `rotate_left`；反向转回 C++ 时，自然落成 `std::rotate`。

也就是说，这里来回一趟后，并没有回到最初那段老代码，而是停在了一个更现代、更短也更清楚的位置。

这个现象很有意思，因为它说明：

**Rust 重写带来的某些收益，不是“Rust 才能做到”，而是“重写这件事本身给了你一次重构和现代化的机会”。**

`std::rotate` 并不是 Rust 发明的，C++98 就有。只是原始项目没有借这次机会去用它，后来的 Rust 重写用了，再转回 C++ 时，这个改进也一起保留下来了。

### 颜色布局：设计哲学变了，但算法没变

原始 fish C++ 的颜色类型很“老派 C++”：强行 bit-pack，类型标签、flag 和颜色数据都挤在极紧的布局里，还会 `static_assert` 控制大小。

Rust 重写把这件事放松了，转成了更清楚的代数数据类型和 `bitflags!`。转写回 C++ 后，也更接近 Rust 的结构：标签更明晰，布局不再那么极限。

于是最后出现的是一种很典型的往返结果：

- 算法没变
- 语义没变
- 但某些为了原语言特性而做的微优化，没有原封不动跟着回来

这不奇怪。因为如果你只看 Rust 源码，本来就看不见“旧 C++ 里有人刻意做过 4-byte packing”这件事。

### timer：几乎是理想控制组

`timer` 模块是这篇里最像“什么都没发生”的例子。

- 取时间点的方式一样
- 计算时间差的方式一样
- 格式化阈值一样
- RAII / Drop 的思路也一样

真正变化的只是字符串类型等外围细节。

所以它很适合当控制组：当代码本身足够算法化、没有太多类型层设计意图时，往返会高度收敛。

## 哪些东西在往返后依然还在

| 模式 | 结果 |
|------|------|
| 命名颜色表 | 三份代码几乎一致 |
| 二分查找颜色表 | 一致 |
| 最邻近颜色匹配 | 一致 |
| `getrusage + steady_clock` 计时 | 一致 |
| GNU getopt 的解析逻辑 | 一致 |
| 长选项精确/歧义匹配 | 一致 |

至少在这几个模块里，算法层面的稳定性已经是一个很重复出现的现象了。

## 哪些东西没有回到原样

| 模式 | 原始 C++ | Rust → 转回 C++ |
|------|---------|-----------------|
| 数组交换 | 手写交换循环 | `std::rotate` |
| 失败返回 | 特殊值约定 | `std::optional` 风格 |
| 颜色内存布局 | 极限压缩 | 更清楚、但更松的布局 |
| 部分参数设计 | 保留旧包袱 | 跟着 Rust 重写一起简化 |

这些变化大致可以分成三类：

**1. 真正保留下来的重构改进。**  
例如 `std::rotate`、`std::optional` 这种，放在什么语言里都算进步。

**2. Rust 重写时做出的设计取舍。**  
某些参数和接口之所以变了，是因为 fish 的 Rust 版本已经做了简化。转回 C++ 时自然会延续这个版本。

**3. 无法从 Rust 源码里“自动复原”的旧优化。**  
比如 bit-packing。它不是算法，而是原始实现的局部优化。既然 Rust 版本已经不再强调它，转写器也不会凭空把它猜回来。

## 从整个 fish 项目往回看

这篇还有一个需要小心的边界：我这里只看了 3 个模块，总共 1331 行 Rust，而 fish 全项目大约是 7.5 万行。严格说，结论只能对这部分材料负责。

但整个项目的行数变化仍然给了一个有意思的背景：fish 的 Rust 版本整体上比更早的 C++ 版本大 16% 到 21% 左右。这个方向和前几篇正好相反。

这提醒我不要把“Rust 比 C++ 更长”或者“C++ 比 Rust 更短”说成语言本体属性。更准确的说法可能是：

- **人工重写** 往往会引入更多显式结构、错误处理和现代化整理；
- **机械或半机械转写** 则常常会把那些只服务于编译器验证的层压扁。

方向不同，结果也不同。

## 这一轮让我更愿意怎么概括

fish 的往返结果，比较像一个“第三版本”：

- 它不是原始 C++
- 也不是 Rust
- 它继承了 Rust 重写后的 API 取向和结构清晰度
- 同时又借现代 C++ 标准库把一部分旧时代写法进一步收短

所以，如果只问这次 fish 重写“主要是不是表达力故事”，我的回答还是偏向于：**不是。**

至少在这三个模块里：

- 算法没有因为语言变化而改变
- 某些更好的设计会在往返中留下来
- 真正不会留下来的，是依赖 Rust 编译器那套验证机制的部分

### 新增映射

| Rust | C++ |
|------|-----|
| 代数 `enum` | `enum class` + tagged union |
| `bitflags!` | 常量 + 手动位运算 |
| `rotate_left` | `std::rotate` |
| `Option<Self>` | `std::optional<T>` |
| fish 的 `wstr` | `std::wstring_view` / `std::wstring` |
| 多生命周期参数 | 消失，变成约定 |
| `Drop` 风格 guard | 析构函数 guard |

### 下一篇

第五篇会把四次实验放在一起，重新整理那条越来越清楚的分界：什么更像“表达力”，什么更像“拒绝力”，以及这件事对现有 C++ 代码库到底意味着什么。

---

*这次往返对照的材料在 [`04-fish-shell`](../04-fish-shell/)。这里比对的是 fish shell 的三个模块：原始 C++、后来的 Rust 重写，以及由 Rust 再转写回来的 C++。*
