就地初始化的四个层次
摘要
文章讨论了 Rust 中就地初始化的四个层次,从原始指针到语言提案如 &uninit/&own,以提高编程中的内存效率和安全性。
<p><a href="https://lobste.rs/s/snhfrq/four_levels_place_initialization">评论</a></p>
查看缓存全文
缓存时间: 2026/08/17 08:07
# 原地初始化的四个层级
来源:https://blog.yoshuawuyts.com/four-levels-of-in-place-initialization
## 介绍
原地初始化的目标是允许类型直接在内存位置中构造,无需额外的移动或复制。在处理大型类型时,这可以提升效率,甚至避免栈溢出。但某些类型具有**地址敏感性**,出于正确性考虑不能移动。关于如何在语言中编码原地初始化存在分歧,各种相互冲突的需求与约束交织,协调起来颇具挑战。我认为解决此问题的正确方式并非引入单一特性,而是建立一个**原地初始化的四级特性层级**。
## 层级 0:原始指针
在最底层,我们有原始指针和 `MaybeUninit`。这是目前编码“放置”最灵活的方式,但代价是几乎失去了其他所有特性。`pin-init` crate 和 `placing` crate 的内部实现都采用了这种方式。更多细节请参阅我关于放置函数的文章(https://blog.yoshuawuyts.com/placing-functions/),其中我详细演示了完整的脱糖过程。
我对这一层级的评价是:“有总比没有好”。尽管存在诸多不足,但生态系统中能有一种编码放置的方式,这本身就是件好事。
以下是使用 `MaybeUninit`、原始指针和 `unsafe` 延迟初始化的基本示例:
```rust
use std::mem::MaybeUninit;
let mut x = MaybeUninit::<A>::uninit(); // 1. 创建未初始化的 `A` 类型占位 `x`
let y: *mut A = x.as_mut_ptr(); // 2. 获取指向 `x` 的原始指针 `y`
unsafe { y.write(A { .. }) }; // 3. 通过 `y` 初始化所有字段
let mut x = unsafe { x.assume_init() }; // 4. 将 `x` 认证为已初始化
let y: &mut A = &mut x; // 5. `x` 已初始化,可正常使用
```
在第 5 步中,我们移动了 `x` 的值。如果希望在不移动的情况下将 `x` 认证为已初始化,需调用 `MaybeUninit::assume_init_mut`,但这会返回 `&mut T` 而非原地修改 `T`。
若没有额外的语言特性,无法在不移动所有权值或将其转换为引用的情况下,将其认证为已初始化。
## 层级 1:引用
原始指针功能强大,但编译器无法验证其正确性,这给程序员带来了额外负担。我们需要一种抽象,既能编码原始指针的大部分功能,又能被编译器在合理时间内静态检查。
我倾向于丁翔飞提出的 `&uninit`/`&own` 引用对方案,但也有其他提案可填补此位置。`&uninit`/`&own` 的理念是:我们可以获取类型的 `&uninit` 引用,待其所有字段初始化完成后,可将其**认证**为 `&own` 引用。
流程分为四步:
1. 创建 `A` 类型的未初始化占位 `x`。
2. 获取 `x` 的 `&uninit` 引用。
3. 初始化值,返回 `&own` 引用。
4. 通过赋值完成初始化认证。
```rust
let x: A; // 1. 创建未初始化的 `A` 类型占位 `x`
let y: &uninit A = &x; // 2. 获取 `x` 的 `&uninit` 引用 `y`
*y = A { .. }; // 3. 初始化 `y` 的所有字段
let y: &own A = y; // 4. 从此刻起引用 `y` 变为 `&own` 类型
x = y; // 5. 将 `x` 认证为已初始化
let y: &mut A = &mut x; // 6. `x` 现已初始化,可正常使用
```
此提案的主要创新在于将未初始化占位提升为可讨论和引用的**一等公民**。上述示例目前可通过 `let a; a = A { ... };` 实现,无需 `&uninit` 和 `&own`。但这在跨函数时无法实现,而 `&uninit`/`&own` 可以:
```rust
// 将 `&uninit A` 转换为 `&own A`
fn init_a<'a>(y: &'a uninit A) -> &'a own A {
*y = A { ... };
y
}
let x: A; // 1. 创建未初始化的 `A` 类型占位 `x`
x = init_a(&x); // 2. 初始化 `x`
let y: &mut A = &mut x; // 3. `x` 现已初始化,可正常使用
```
这并非简单特性,对应的问题同样复杂。此方案使未初始化值成为可安全传递和初始化的**一等公民**。设计上它追求最大表达力,这意味着将控制力置于首位。
## 层级 2:放置函数
引用优先考虑控制力,而放置函数则优先考虑易用性。
放置函数(https://blog.yoshuawuyts.com/placing-functions)会重写 `return` 关键字,将数据写入输出指针而非复制。它可通过原始指针或 `&uninit`/`&own` 引用实现,但不同于这两者,它无需修改函数签名。
要理解其价值,需思考如何将现有代码迁移至放置模式。以下是一个返回 `A` 类型值并赋值给变量 `x` 的典型函数:
```rust
// 创建 `A` 类型值
fn init_a() -> A {
A { ... }
}
let x = init_a(); // 1. 创建 `A` 类型值
```
对比使用 `&uninit` 和 `&own` 的原地初始化示例,可见此方式简洁得多。无需复杂的引用、生命周期和认证步骤。但遗憾的是,它仍涉及复制,若 `A` 包含大量字段可能成为问题。因此理想情况下,我们希望有一种能放置且无需繁琐仪式的方式:
```rust
// 原地创建 `A` 类型值
#[emplace]
fn init_a() -> A {
A { ... }
}
let x = init_a(); // 1. 原地创建 `A` 类型值
```
不错吧?当然这不如 `&uninit` + `&own` 灵活,但对于常见场景已足够。我们可能不希望这仅是一个一次性属性,而应成为独立关键字。
我目前的思路是将其编码为类似 `const` 的效应,并通过 `with` 关键字(https://blog.yoshuawuyts.com/a-with-based-effect-notation)暴露所有效应:
```rust
// 原地创建 `A` 类型值
fn init_a() -> A with emplace {
A { ... }
}
let x = init_a(); // 1. 原地创建 `A` 类型值
```
标注 `emplace` 效应的函数保证将返回值写入输出指针而非复制。这使得这些函数能“返回” `!Move` 类型(https://blog.yoshuawuyts.com/self-referential-types/#immovable-types),这是安全构造无条件自引用类型(https://rust-for-linux.com/the-safe-pinned-initialization-problem)的必要条件。
## 层级 3:自动移动消除
在 RFC 3943(https://github.com/Amanieu/rfcs/blob/mir-move-elimination/text/0000-mir-move-elimination.md)中,Amanieu 提议添加 **MIR 移动消除**。这使编译器能在 MIR 层面自动消除移动作为优化,可应用于之前讨论的部分示例:
```rust
// 创建 `A` 类型值
fn init_a() -> A {
A { ... } // 假设 `A` 大小为 2kB
}
let x = init_a(); // 1. 优化器确保 `x` 被原地创建
```
这看起来与“放置函数”提案相似,但作为优化实现。优化应仅影响程序性能而非语义,因此不可依赖。这意味着从 MIR 移动消除函数返回 `!Move` 类型仍会被禁止,因为消除是编译器的实现细节而非语言组成部分。
**若某些新行为对语言正确性至关重要,应通过记法明确表达。**即使我们能保证特定表达式总是放置,只要存在例外,很快就会陷入解释左值、右值、纯右值、泛左值和亡值之间差异的困境(https://blog.yoshuawuyts.com/four-levels-of-in-place-initialization#cpp)。
我认为在代码中明确写出要求,比期望程序员从上下文推断更优。或许未来 Rust 能保证每个位置的每个表达式都支持放置,届时主动选择放置的记法将变得多余,我们可能通过版本迭代(https://blog.yoshuawuyts.com/four-levels-of-in-place-initialization#opt-out)将其设为表达式的默认行为。但仅凭单一特性无法立即实现,最好先从两个互补特性起步,这与我们处理 `const` 的方式类似。
## 结论
我们希望原地初始化是为了能处理自引用类型(要求内存中不可移动),并通过消除复制提升语言整体性能。
以下是本文讨论的四层原地初始化特性:
| 表达力 | 内存 | 签名 | 语义 |
|--------|------|------|------|
| **0. 原始指针** | 广泛 | Unsafe | 改变 | 保证 |
| **1. 引用** | 广泛 | Safe | 改变 | 保证 |
| **2. 放置函数** | 基础 | Safe | 不变 | 保证 |
| **3. MIR 移动消除** | 基础 | Safe | 不变 | 不保证 |
1. **原始指针**是我们当今使用的 unsafe 工具,作为最终逃生通道。
2. **引用**是编译器能检查的安全指针子集。
3. **放置函数**保证放置,不改变函数输入输出,仅改变效应。
4. **MIR 移动消除**是加速现有代码的尽力优化。
Rust 试图平衡易用性、表达力与控制力。语言中已有指针,编译器应始终进行尽可能的优化。通过将安全抽象拆分为高级与低级特性,我们可确保简单场景易于处理,同时为需要者保留控制力。
相似文章
在Rust中,main函数之前与之后的生命
深入探讨Rust二进制文件中main函数之前所发生的事情,探索运行时初始化、入口点以及可变数据初始化的新颖技术。
安全变得简单 第1部分:单一所有权(并非)可选
本文介绍了一种基于线性类型和抽象解释的内存安全新方法,旨在比Rust更符合人机工程学原理地消除诸如释放后使用和内存泄漏等常见错误。
@debasishg:我关于Rust底层系统设计系列的第一部分现已发布 - 这部分涵盖:• 如何根据谁接触什么来布局共享的Rust结构体…
关于Rust底层系统设计系列的第一部分介绍了缓存感知的数据布局技术,包括字段分区以避免伪共享,重点涉及多线程结构体和128字节规则,并以SPSC环形缓冲区为例。
Rust 项目目标:不可移动类型与保证析构函数
Rust 项目目标概述了不可移动类型和保证析构函数的计划,以提升语言安全性和资源管理。
内存安全绝对主义者
本文批评了编程语言辩论中的内存安全绝对主义,认为像 Fil-C 这样的新方法也有权衡,而将 Rust 视为不安全忽略了实际好处。