Four levels of in-place initialization

Four levels of in-place initialization

原地初始化的四个层级

Introduction

The goal of in-place initialization is to enable the construction of types directly into a memory location without any additional moves or copies. When working with big types this can be more efficient and even prevent stack overflows. But some types are what we call address sensitive and so cannot be moved for correctness reasons. There is some disagreement about how we should encode in-place initialization in the language. There are conflicting requirements and constraints at play, and reconciling those is tricky. I believe that the right way to attack the problem space is not by introducing a single feature, but by introducing a 4-level feature hierarchy for in-place initialization.

引言

原地初始化的目标是允许直接在内存位置构造类型,而无需任何额外的移动或拷贝。在处理大型类型时,这可以提高效率,甚至防止栈溢出。但有些类型被称为“地址敏感型”(address sensitive),出于正确性考虑,它们不能被移动。关于如何在语言中实现原地初始化,目前还存在一些分歧。各种需求和约束相互冲突,协调这些问题非常棘手。我认为解决这一问题的正确方法不是引入单一特性,而是为原地初始化引入一个四层级的特性体系。


Level 0: Raw pointers

At the lowest level we have raw pointers and MaybeUninit. This is by far the most flexible way to encode emplacement, but it comes at the cost of virtually everything else. This is both how the pin-init crate and placing crate are implemented internally. To read more about this see my post on placing functions where I work through a full desugaring. The way I categorize this level is as: “It’s better than nothing”. It’s good that we have some way to encode emplacement in the ecosystem today, even if it leaves much to be desired. Here is a basic example using MaybeUninit, raw pointers, and unsafe to defer initialization:

第 0 层:裸指针

在最底层,我们有裸指针和 MaybeUninit。这是目前实现原地放置(emplacement)最灵活的方式,但代价是牺牲了几乎所有其他特性。pin-initplacing crate 的内部实现均采用了这种方式。若想了解更多,可以参阅我关于放置函数(placing functions)的文章,我在其中进行了完整的去语法糖分析。我将这一层级归类为:“总比没有好”。很高兴生态系统中目前有办法实现原地放置,尽管它还有很多不尽如人意的地方。以下是一个使用 MaybeUninit、裸指针和 unsafe 来推迟初始化的基本示例:

use std::mem::MaybeUninit; 
let mut x = MaybeUninit::<A>::uninit(); // 1. Create an uninit place `x` of type `A`
let y: *mut A = x.as_mut_ptr();         // 2. Take a raw pointer `y` to `x`
unsafe { y.write(A { .. }) };           // 3. Initialize all fields through `y`
let mut x = unsafe { x.assume_init() }; // 5. Notarize `x` as initialized
let y: &mut A = &mut x;                 // 6. `x` is initialized and can be used as normal

On step 5 we do move the value of x. If wanted to notarize x as initialized without moving it, we would need to call MaybeUninit::assume_init_mut, but this returns an &mut T rather than change T in-place. Without additional language features, it’s impossible to notarize an owned value as initialized without moving it or turning it into a reference.

在第 5 步,我们确实移动了 x 的值。如果想在不移动的情况下将 x 标记为已初始化,我们需要调用 MaybeUninit::assume_init_mut,但这会返回一个 &mut T,而不是原地改变 T。在没有额外语言特性的情况下,不可能在不移动或不将其转换为引用的情况下,将一个拥有所有权的值标记为已初始化。


Level 1: References

Raw pointers are very powerful, but the compiler cannot check their correctness which places an additional burden on the programmer. What we need is an abstraction that can encode most of what raw pointers can, but in a way that the compiler can statically check it within a reasonable amount of time. My preferred proposal for this is Ding Xiang Fei’s &uninit / &own reference pair, but there are more proposals that could fill this slot. The idea of &uninit/&own that we can take an &uninit reference to a type, and once all of its fields have been initialized can then be notarized into an &own reference.

第 1 层:引用

裸指针功能强大,但编译器无法检查其正确性,这给程序员带来了额外的负担。我们需要一种抽象,它既能实现裸指针的大部分功能,又能让编译器在合理时间内进行静态检查。我首选的方案是丁祥飞(Ding Xiang Fei)提出的 &uninit / &own 引用对,当然还有其他方案也能填补这个位置。&uninit/&own 的核心思想是:我们可以获取一个类型的 &uninit 引用,一旦其所有字段都被初始化,就可以将其“公证”(notarize)为 &own 引用。

let x: A;                       // 1. Create an uninit place `x` of type `A`
let y: &uninit A = &x;          // 2. Take an `&uninit` reference `y` to `x`
*y = A { .. };                  // 3. Initialize all fields of `y`
let y: &own A = y;              // 4. The reference `y` is `&own` from here on out
x = y;                          // 5. Notarize `x` as initialized
let y: &mut A = &mut x;         // 6. `x` is now initialized and can be used as normal

The main innovation of this proposal is that it makes uninitialized places a first-class thing we can talk about and reference. The example above can already be written today without &uninit and &own by writing let a; a = A { ... };. But this doesn’t work across functions, which is something we can do with &uninit/&own:

该提案的主要创新在于,它使“未初始化的内存位置”成为我们可以讨论和引用的“一等公民”。上述示例在今天不使用 &uninit&own 的情况下,通过 let a; a = A { ... }; 也可以实现。但这无法跨函数工作,而使用 &uninit/&own 则可以做到:

// Convert an `&uninit A` into an `&own A`. 
fn init_a<'a>(y: &'a uninit A) -> &'a own A { *y = A { ... }; y } 
let x: A;                       // 1. Create an uninit place `x` of type `A`
x = init_a(&x);                 // 2. Initialize `x`
let y: &mut A = &mut x;         // 3. `x` is now initialized and can be used as normal

This is not a simple feature, but it’s not a simple problem either. This makes uninitialized values both first-class and safe to pass around and initialize. By design it wants to be as expressive as possible, which means prioritizing control above all else.

这不是一个简单的特性,但它解决的问题也不简单。它使未初始化的值既成为一等公民,又能安全地传递和初始化。其设计初衷是尽可能提高表达能力,这意味着将控制权置于首位。


Level 2: Placing Functions

Where references prioritize control, placing functions prioritize ergonomics. Placing functions are functions which re-write the return keyword to write data to an out-pointer rather than copying. It can be implemented in terms of either raw pointers or &uninit/&own references. But unlike either of those features it doesn’t require any further changes to the function signature.

第 2 层:放置函数

如果说引用层级优先考虑控制权,那么放置函数则优先考虑人体工程学(易用性)。放置函数是指重写 return 关键字,将数据直接写入输出指针(out-pointer)而非进行拷贝的函数。它可以基于裸指针或 &uninit/&own 引用来实现。但与前两者不同,它不需要对函数签名进行任何额外修改。

To show where this is useful we need to think about how we would transition existing code to emplace. Here is a typical function which returns a value of type A, and assigns it to the variable x.

为了展示其用途,我们需要考虑如何将现有代码迁移到原地放置。这是一个典型的返回 A 类型值并将其赋值给变量 x 的函数:

// Create a value of type `A` 
fn init_a() -> A { A { ... } } 
let x = init_a(); // 1. Create a value of type `A`

If you compare this to the in-place init example using &uninit and &own, you’ll notice just how much simpler this is. No fancy references, lifetimes, and notarization. But unfortunately it also copies, which if A contains many fields might be a problem. So ideally we’d have something that can emplace but without all the ceremony:

如果你将其与使用 &uninit&own 的原地初始化示例进行比较,你会发现它简单得多。没有复杂的引用、生命周期和公证过程。但不幸的是,它仍然会进行拷贝,如果 A 包含许多字段,这可能会成为问题。因此,理想情况下,我们希望有一种既能原地放置又无需繁琐步骤的方法:

// Create a value of type `A` in-place 
#[emplace] 
fn init_a() -> A { A { ... } } 
let x = init_a(); // 1. Create a value of type `A` in-place

Not bad, right? Of course this isn’t as flexible as &uninit+&own. But for the common cases this should be plenty. Though we probably don’t just want this to be a one-off attribute, but probably its own keyword. My current thinking is that we should encode this as an effect like const, and expose all effects using the with keyword:

不错吧?当然,这不如 &uninit+&own 灵活。但对于常见情况,这已经足够了。不过,我们可能不希望这仅仅是一个一次性的属性,而应该是一个独立的关键字。我目前的想法是将其编码为像 const 那样的“效果”(effect),并使用 with 关键字来暴露所有效果:

// Create a value of type `A` in-place 
fn init_a() -> A with emplace { A { ... } } 
let x = init_a(); // 1. Create a value of type `A` in-place

A function annotated with the emplace effect guarantees that it will write its return value to an out-pointer rather than copying.

带有 emplace 效果标注的函数保证会将返回值写入输出指针,而不是进行拷贝。