就地初始化的四个层次

Lobsters Hottest 工具

摘要

文章讨论了 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 试图平衡易用性、表达力与控制力。语言中已有指针,编译器应始终进行尽可能的优化。通过将安全抽象拆分为高级与低级特性,我们可确保简单场景易于处理,同时为需要者保留控制力。

相似文章

内存安全绝对主义者

Lobsters Hottest

本文批评了编程语言辩论中的内存安全绝对主义,认为像 Fil-C 这样的新方法也有权衡,而将 Rust 视为不安全忽略了实际好处。