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

## Part 5：关于表达力、拒绝力，以及一条可能路径的几点笔记

*Su Qingyue*

---

## 先把材料摆平

四次实验里，一共处理了大约 9300 行 Rust，覆盖了四个项目、八个具体组件。

| 文章 | 项目 | Rust 行数 | C++ 行数 | 测试 |
|------|------|----------:|----------:|-----:|
| 1 | hexyl | 2,392 | 2,111 | 26/26 |
| 2 | coreutils（3 个工具） | 2,217 | 1,530 | 66/66 |
| 3 | mini-redis | 3,393 | 2,266 | 10/10 |
| 4 | fish shell（3 个模块） | 1,331 | 1,087 | n/a |
| **总计** | | **9,333** | **6,994** | **102/102** |

把这一组数字放在一起，至少能先看到几件事：

- 行为测试没有在转写后立刻崩掉；
- 大多数算法都保住了；
- C++ 版本通常更短；
- 唯一明确需要架构级重做的，是 mini-redis 里的 `tokio::select!` 动态 fan-in。

我不想把这些材料说得比它们本身更大。它们不是证明，只是一组够具体、够完整的样本。但它们已经足够让我把一些原本混在一起的说法拆开。

## 经常被混在一起的两件事

围绕 Rust 和 C++ 的讨论里，最常见的是把下面两件事合成一句话：

1. Rust **更有表达力**
2. Rust **更能拒绝危险程序**

这两件事不是一回事。

### 表达力

如果把“表达力”说得很严格，那么 Rust 和 C++ 都是图灵完备，这个问题就没什么意思了。真正有意思的是一种更工程化的理解：

**一个语言里的关键构造，能不能在另一个语言里，通过局部、机械、可理解的方式复现出来？**

如果可以，那么至少在这个问题域上，两边的“实际表达力”差距就没有想象中那么大。

四次实验里，大部分地方确实是这样：

- 数据结构能找到对应物
- 错误路径能换一种方式表达
- 算法本身没有大规模重写
- 行为测试还能对上

只有 mini-redis 里的 `select!` 明显突破了“局部替换”的范围，需要进入并发架构设计。

所以我更愿意说：

**在这批样本里，Rust 的很多程序结构并没有表现出“C++ 无法表达”的性质。差别存在，但没有被材料支持到那种绝对强度。**

### 拒绝力

“拒绝力”说的则是另一件事：

**哪些程序，Rust 编译器会直接拒掉，而 C++ 编译器会让它过去。**

这恰恰是 Rust 最硬的优势：

- 生命周期错误会被拦住
- 非线程安全对象跨线程传递会被拦住
- 独占借用冲突会被拦住
- 很多 dangling / aliasing 风险会在编译阶段就被限制住

这部分在四次转写里，几乎没有任何一次能跟着源码结构一起“保留下来”。  
结构常常还在，行为也可能还在，但**强制验证**不在了。

因此，如果要把两种说法分开，我现在会更偏向：

- 在这批样本里，Rust 的**表达优势**没有被强力证明出来；
- Rust 的**拒绝优势**反而被一次次看得更清楚。

## 转写里反复出现的四类情况

为了让这个判断更具体，我把遇到的构造粗略分成四类。

### 1. 直接对应物

这一类几乎可以一对一换过去：

| Rust | C++ |
|------|-----|
| `Option<T>` | `std::optional<T>` |
| `Result<T, E>` | `std::expected<T, E>` |
| `Vec<T>` | `std::vector<T>` |
| `String` / `&str` | `std::string` / `std::string_view` |
| 简单 `enum` | `enum class` |
| `const fn` | `constexpr` |
| `Box<dyn Trait>` | `std::unique_ptr<Interface>` |

这部分是为什么前几篇大量代码都还能顺着走。

### 2. 会塌掉的抽象层

这一类在 Rust 里往往存在，是为了把某个安全性或统一性条件写给编译器；到了 C++，行为还在，但包装会明显压扁：

| Rust | C++ | 变化 |
|------|-----|------|
| 带生命周期和 writer 泛型的 printer | `ostream&` | 生命周期和泛型一起消失 |
| `Pin<Box<dyn Stream>>` | 消失 | 不再需要那层 pin / dyn stream 结构 |
| 多层 trait hierarchy | 几个直接函数 | 组合层被压扁 |
| `StreamMap` | 订阅列表 | fan-in 自己实现 |
| `thiserror` 错误体系 | 直接打印或简化错误对象 | 类型层次塌掉 |

这部分解释了为什么 C++ 版本经常更短。

### 3. 安全注记

这一类几乎完全不会“被带过去”：

| Rust | C++ |
|------|-----|
| `&'a T` | `const T&`，但没有生命周期证明 |
| `&'a mut T` | `T&`，但没有独占保证 |
| `T: Send + Sync` | 没有标准等价注记 |
| `pub(crate)` | 约定 |
| `#[non_exhaustive]` | 通常消失 |

这里是 Rust 真正最有价值、也最不容易迁移的那部分。

### 4. 组合原语

这一类最少，但最关键：

| Rust | C++ | 差别 |
|------|-----|------|
| `tokio::select!` | 自己搭共享唤醒机制 | 并发组合不再是一等原语 |
| `tokio::sync::broadcast` | 自定义广播通道 | 标准库没有现成对应物 |

到目前为止，真正逼我承认“这里已经不是简单替换”的，主要就是这类。

## 三个越来越稳定的观察

### 1. 算法比语言更稳定

这四次实验里，真正稳定得惊人的，是算法。

无论是：

- `cat` 的行号进位
- `tr` 的 256 项字符表
- 命名颜色表的二分搜索
- 最近颜色匹配
- RESP 协议解析
- 过期时间管理
- GNU getopt 的参数排列

这些东西都没有因为语言变化而换掉。

这并不神秘。算法本来就是更抽象的一层。但把它真放在几种语言和几次转写里对照之后，这个事实会比平时想象得更扎实。

### 2. 安全更像编译流程属性，而不是源码表面属性

这是整组实验里，我感受最强的一点。

同一个 Rust 程序，在通过借用检查、生命周期检查、Send/Sync 检查之后，它的“安全性”其实来自一整个验证流程。转成 C++ 后，很多运行行为并没有变，数据流也没有凭空改掉，但那套验证流程不见了。

所以更贴切的说法可能不是“安全写在源码里”，而是：

**源码提供了足够的信息，让某个编译器或验证器去检查它。**

一旦验证器换了，安全性就不再自动成立。

这并不意味着源码不重要，而是意味着：  
把语言、编译器和验证流程捆成一个整体去看，往往比只盯着语法表面更接近事实。

### 3. 设计改进更容易跨语言保留下来

fish 那一篇让我更确定了这一点。

像 `std::rotate` 这种改进，或者用 `optional` 明确表达“可能失败”，这些东西一旦出现，往返之后仍然会留下。

而依赖某个特定编译器验证机制的收益，往往一换语言就散了。

所以如果把收益分层，大概会更像这样：

1. **设计和 API 层面的改进**，最容易迁移
2. **语言局部优化**，有时能迁移，有时不能
3. **编译器验证带来的保证**，最难直接迁移

## 一条我觉得值得想，但还远远谈不上证成的路

到这里，我会自然想到一个问题。

如果 Rust 最硬的价值，主要体现在验证；如果大量程序结构又能在语言间来回搬，那么对现实世界里那些不太可能整体重写成 Rust 的 C++ 代码库来说，是否存在一种更折中的工作流？

### 一个可能的工作流

非常粗略地说，它像这样：

1. 现有系统还是 C++
2. 借助 AI 或转写工具，把局部模块转成 Rust
3. 用 Rust 编译器和测试去逼出问题
4. 修正后，再回到 C++ 版本继续维护
5. 周期性地重新验证，而不是指望一次性完成迁移

这条路**不意味着“C++ 因此就等于 Rust”**。  
它真正想说的只是：

对那些永远不可能完整重写的存量代码，也许还能借更强的验证环境做一次次“借道检查”。

### 它成立需要什么

最少得有三样东西：

**1. 足够可靠的转写。**  
前几篇说明它在不少代码上可以工作，但 mini-redis 也提醒了并发组合并不好啃。

**2. 足够认真的测试。**  
没有测试，根本谈不上“转过去又转回来还是同一个程序”。

**3. 重复验证的机制。**  
如果 C++ 代码继续演化，它当然会偏离一开始那份被验证过的结构。那就只能周期性重做，而不是幻想“一次通过，永远安全”。

### 它不能替代什么

这条路不能替代 Rust 的日常增量验证；也不能让“以后继续在 C++ 里随便写”突然变成一件无风险的事。

它更像是给现实世界里那些：

- 团队已经全是 C++；
- 构建链路和部署系统都绑死在 C++；
- 不可能整体迁走；
- 但又确实想把某些关键模块整理得更稳

的场景，提供一个也许可操作的中间方案。

### 最直接的反问

一个很自然的问题是：

**“都转到 Rust 了，为什么不直接留在 Rust？”**

对新项目、小项目，答案完全可能就是：那就直接留在 Rust。

但对于大型存量 C++ 代码库，这个问题往往不只是技术问题，还包括：

- 组织和团队技能结构
- 既有构建系统
- 已经存在的基础设施和依赖
- 维护成本和迁移窗口

所以“第三条路”并不是要和 Rust 竞争。它只是承认：现实里很多场景面对的不是几个理想选项，而是“能不能先往前走一点”。

## 这一系列里反复出现的一条分界线

如果让我把四篇实验压成一句更稳一点的话，大概会是：

**Rust 让你能写什么**，在这批材料里看起来并没有和 C++ 拉开想象中的巨大鸿沟。  
**Rust 不让你写什么**，反而是更清楚、更结实的优势。

而这又进一步把问题引到了一个更少被直接说清的地方：

**很多收益，未必只属于“语言表面”，而是属于“语言 + 编译器 + 验证流程”这一整套东西。**

这也就是为什么我会越来越少把这组材料理解成“语言战争”的材料，反而更把它看成“验证边界在哪儿”的材料。

## 结语

这组实验一开始的问题其实很简单：

**Rust 程序能不能较为机械地转回 C++？**

到目前为止，我能接受的回答是：

- 大多数时候，可以；
- 在并发组合等少数点上，不再是简单替换，而是需要设计；
- 转写后通常还能保住算法和行为；
- 真正没有一起跟过去的，是 Rust 编译器替程序员做掉的那部分验证。

所以我不会把这组材料读成“Rust 没那么重要”。恰恰相反，它让我更容易把 Rust 的价值说清楚：

它最关键的部分，在这批材料里，少有地不是“写法看起来更新”，而是“验证强度明显更高”。

而如果这点成立，那么更大的开放问题也许就变成了：

**验证能力，是否只能和语言绑死在一起；还是说，在一些现实工作流里，它可以被部分迁移、复用，或者借道带进别的代码库？**

这个问题，我没有答案。  
但至少现在，它已经不再只是抽象讨论了。

---

*这一篇对应的是整个系列的汇总材料。四组实验的代码和参考目录都保留在仓库根目录里。当前整理出的行为测试一共 102 个。*

## 附录：C++ 能不能模拟 Rust 的 `Send`？

这一系列里，有个特别关键、但也特别容易被一句话带过去的点：`Send`。

更早的草稿里，我把这一段写得更像“未来展望”。到 **2026 年 4 月 5 日**，我愿意把话说得更实一些：在我本地一个 GCC 16 trunk 构建（`g++ 16.0.1 20260324 (experimental)`，配合 `-freflection`）里，这件事已经不只是纸上推演了。仓库根目录里的 [`gcc-static-reflection.cpp`](../gcc-static-reflection.cpp) 就是这次用来做 smoke test 的参考文件。

### `Send` 真正厉害在哪儿

Rust 的 `Send` 不是一个普通标签。它真正厉害的地方，是**编译器会递归地看一个类型的字段，自动推导它能不能跨线程安全转移**。

也就是说，程序员不需要把每一层都手写标记出来。只要字段里混进一个不该跨线程移动的东西，整个类型就不再是 `Send`，编译器会直接拦住。

这个机制不是“并发库好用”那么简单，它背后是一种非常强的、递归展开的编译期判定能力。

### 传统 C++ 为什么没有自然对应物

传统 C++ 缺的，不只是一个同名 trait，而是两件事同时缺：

1. 没有 Rust 那样统一的所有权 / 线程安全语义作为推导基础
2. 没有现成的、标准化的递归字段反射来自动生成这种判断

所以现实里的 C++ 做法通常会退回去：

- 依赖约定
- 依赖代码 review
- 依赖外部静态分析器
- 或者只在局部类型上手写规则

这和 Rust 的“默认递归推导”不是同一个强度。

### GCC 16 已经把这件事推进到了什么程度

如果把 P2996 这套静态反射按现在 GCC 16 的实验实现来看，情况已经不是“理论上也许可以”了。更准确的说法是：

- 你已经可以枚举类型成员；
- 也已经可以在编译期对成员做递归检查；
- `std::define_static_array` 这样的 workaround 能把当前实现里的成员列表稳定地变成可迭代形式；
- 因而一个最小版的 `Send` 风格自动派生，已经能被真实编译器编出来。

一个足够小、并且我已经本地编过的原型，大概像这样：

```cpp
#include <meta>
#include <type_traits>

struct LocalRcInt {};

consteval bool is_sendable_info(std::meta::info r) {
    return r == ^^int || r == ^^bool;
}

template <typename T>
consteval bool derive_sendable() {
    if constexpr (!std::is_class_v<T>)
        return is_sendable_info(^^T);

    constexpr auto ctx = std::meta::access_context::current();
    bool result = true;

    template for (constexpr auto m :
        std::define_static_array(std::meta::nonstatic_data_members_of(^^T, ctx))) {
        if constexpr (std::meta::type_of(m) == ^^LocalRcInt)
            result = false;
        else if constexpr (!is_sendable_info(std::meta::type_of(m)))
            result = false;
    }
    return result;
}

template <typename T>
concept Sendable = derive_sendable<T>();

struct GoodState { int score; bool live; };
struct BadState { int score; LocalRcInt cache; };

static_assert(derive_sendable<GoodState>());
static_assert(!derive_sendable<BadState>());
```

这段代码故意写得很小。它没有尝试完整覆盖标准库容器，也没有把所有库类型都卷进来。原因不是思路不成立，而是当前实验实现边缘还比较粗糙，越是库味重、alias 多的类型，越容易碰到实现细节。

但关键点已经够清楚了：

**“递归看字段，再做编译期约束” 这套机制，现在已经可以在真实工具链里跑起来。**

但这仍然不等于 Rust 的 `Send` 原样搬过来。因为即便反射能力已经落地到可测试，C++ 也还缺少 Rust 那种整套一致的别名、所有权和线程语义约束。

所以更现实的说法可能是：

**今天的实验性 C++ 已经能部分模拟 `Send` 背后的自动检查思路，但它仍然很难在现有语言语义上，原封不动复刻 Rust 那种默认、统一、递归推导、而且不可绕过的效果。**

### 真正还差在哪儿

真正的缺口，现在已经不主要是“能不能表达这个检查”，而是“能不能把这个检查变成默认且强制的边界”。

你当然可以造一个 `safe_spawn`，要求参数满足 `Sendable`。  
但标准库里的 `std::thread` 不会因此自动被改写，团队也始终可以绕过你造的那层门。

Rust 的关键优势，不只是“它也能做字段递归检查”，而是：

- 标准线程 API 本身就把 `Send` 写进签名里；
- 默认路径就是受检查路径；
- 想绕过去，必须显式进入 `unsafe`。

也就是说，**C++ 现在越来越像“能表达这套规则”，但 Rust 仍然更像“默认拒绝不满足这套规则的代码”。**

### 这个附录和正文的关系

之所以把 `Send` 单独拿出来，是因为它刚好站在这组实验的边界上。

一方面，正文已经反复说明：Rust 大量安全注记在转写后会消失。  
另一方面，`Send` 又提醒我们：这些注记不是装饰，而是 Rust 能在并发上提供强保证的重要原因。

所以这一组材料最后并不只是让我觉得“很多东西能转回去”。它也不断提醒我：

**能转回去，不代表原本那套检查也会无损跟着过去。**
