ManuallyDrop::takeについて調べたのでまとめた (Rust)

はじめに

MaybeUninitのassume_initメソッドについて調べたのでまとめたの内容と少し被っています。Rust 1.42でStabilized APIsになったManuallyDrop::takeについて調べたのでまとめました。

ManuallyDrop構造体

ManuallyDrop構造体はコンパイラに自動で値の破棄をさせたくない場合に使います。定義では以下のようになっています。

pub struct ManuallyDrop<T: ?Sized> {
    value: T,
}

valueとしてT型の値を単純に持っているだけです。構造体を作成する時はnew()を呼び出します。

pub const fn new(value: T) -> ManuallyDrop<T> {
    ManuallyDrop { value }
}

値を取り出したい時はManuallyDrop::into_innerを呼び出します。すると中の値をMoveして取り出せます。その値を破棄する場合はlet _ =で束縛しないで捨てます。ManuallyDrop::dropでも破棄できますが、into_innerと違いunsafeであり、破棄後に中の値にアクセスしないように自分で管理する必要があります。

ManuallyDrop::take

ManuallyDrop::takeはunsafeな関数でManuallyDropの持つvalueのコピーを返します。

定義は以下です。ptr::readslot.valueの参照を渡しています。

pub unsafe fn take(slot: &mut ManuallyDrop<T>) -> T {
    ptr::read(&slot.value)
}

ptr::read

ptr::readは渡した値のコピーを返します。

定義は以下です。渡されたslot.valueの参照をイミュータブルなRaw Pointerである*const T型の変数srcで受け取ります。適当な領域であるtmpを用意してcopy_nonoverlappingでsrc(slot.valueの中身)をコピーし、返します。

pub unsafe fn read<T>(src: *const T) -> T {
    let mut tmp = MaybeUninit::<T>::uninit();
    copy_nonoverlapping(src, tmp.as_mut_ptr(), 1);
    tmp.assume_init()
}

copy_nonoverlappingは組み込み関数で以下のような型です。

pub unsafe fn copy_nonoverlapping<T>(src: *const T, dst: *mut T, count: usize)

srcからdstcount * size_of::<T>()バイト分をコピーします。ptr::readの定義でcountを1にしているのはここではslot.valuelの分ということです*1

MaybeUninit::as_mut_ptMaybeUninit共有型に格納されている中の値のミュータブルなアドレスを返します。

MaybeUninitを初期化したら最後にtmp.assume_init()で初期化された値を返しています。

ManuallyDrop::takeで気をつけたいこと

特に自分が直面したわけではないですが、こういうことも起こり得るかなということを書きます。

#![allow(unused)]

use std::mem::ManuallyDrop;

fn main() {
    let mut orig_val = ManuallyDrop::new("something".to_string());
    println!("{:?}", orig_val);
    unsafe {
        let taked_val = ManuallyDrop::take(&mut orig_val);
    }
    println!("{:?}", orig_val);
}

上記のプログラムでlet taked_val = ManuallyDrop::take(orig_val)した後にtaked_valの値がdropされると中で保持していたStringも破棄されます。なのでorig_valにアクセスすると不正な値を参照してしまうかもしれません。

f:id:gkuga:20200322163923p:plain

掲載したプログラムを動かすと以下のようになりました。

$ cargo run
    Finished dev [unoptimized + debuginfo] target(s) in 0.01s
     Running `target/debug/manually_drop`
ManuallyDrop { value: "something" }
ManuallyDrop { value: "\u{0}\u{0}\u{0}\u{0}\u{0}\u{0}\u{0}\u{0}\u{10}" }

おわりに

Stable APIになったタイミングで実装が少し変わっていました。

     pub unsafe fn take(slot: &mut ManuallyDrop<T>) -> T {
-        ManuallyDrop::into_inner(ptr::read(slot))
+        ptr::read(&slot.value)
     }

ManuallyDropをコピーして中の値を取り出していたのをManuallyDropが保持するvalueをコピーするようにしたというリファクタリングですね*2

*1:こういう使い方用の直感的にわかる名前の関数でラップしてほしい気もする

*2:ですよね?