読者です 読者をやめる 読者になる 読者になる

テストステ論

高テス協会会長が, テストステロンに関する情報をお届けします.

(rust report) コピーを省略するためにBoxを返すのは意味ない

言ってることが若干怪しいシリーズ.

Rust BookのBox Syntax and Patternsにこんなことが書いてある.

#![feature(box_syntax)]

struct BigStruct {
    one: i32,
    two: i32,
    // etc
    one_hundred: i32,
}

fn foo(x: Box<BigStruct>) -> BigStruct {
    *x
}

fn main() {
    let x = Box::new(BigStruct {
        one: 1,
        two: 2,
        one_hundred: 100,
    });

    let y: Box<BigStruct> = box foo(x);
}

This gives you flexibility without sacrificing performance.

You may think that this gives us terrible performance: return a value and then immediately box it up ?! Isn't this pattern the worst of both worlds? Rust is smarter than that. There is no copy in this code. main allocates enough room for the box, passes a pointer to that memory into foo as x, and then foo writes the value straight into the Box. This is important enough that it bears repeating: pointers are not for optimizing returning values from your code. Allow the caller to choose how they want to use your output.

「Boxを返すようにしても意味ない. Cではコピーを防ぐためにpointerを返したりするがRustでは無意味」が結論で, その理由は, 「mainがBoxに対して領域を確保して, 関数内部からその中にダイレクトに書き込むからだ」とのこと.

まず, 値を返すということがどういうセマンティクスを持っているか?は, おそらくmoveだと思う.

References and Borrowing

この章では, referenceの重要さを考えるためにまず,

fn foo(v1: Vec<i32>, v2: Vec<i32>) -> (Vec<i32>, Vec<i32>, i32) {
    // do stuff with v1 and v2

    // hand back ownership, and the result of our function
    (v1, v2, 42)
}

という, moveされたものをさらに返すという無意味なコードを書いているのだが, それがborrowと等価ならば, 「returnすることによって, moveされてきたv1, v2を再度moveすることによって, 所有権をそのままお返しした」と考えるのが自然であり, returnする時にはmoveしてる可能性が高い.

実験のために, さきほどのBigStructの例を修正する.

#[derive(Debug)]
struct BigStruct {
    one: i32,
    two: i32,
    // etc
    one_hundred: i32,
}

fn foo(x: Box<BigStruct>) -> BigStruct {
    *x
}

fn main() {
    let x = Box::new(BigStruct {
        one: 1,
        two: 2,
        one_hundred: 100,
    });

    let y = Box::new(foo(x));
    
    // error[E0382]: use of moved value: `x`
    // println!("x: {:?}", x);
    println!("y: {:?}", y);
}

[www.reddit.com/r/rust/comments/4cqq50/syntax_of_dereferencing_a_box/] (http://pelicanmemo.hatenablog.com/entry/2016/10/07/183000 はてなのバグを回避)

のコメントによると, "Box's dereference method actually moves instead of borrows."ということだから, このコードは,

  1. fooにxがmoveされた (この時点でxはアクセス不能)
  2. *xによってさらにmoveした (怪しい)
  3. returnすることによってさらにmoveした
  4. Box::newは, 入力されたデータをヒープにコピーする
impl<T> Box<T> {
    /// Allocates memory on the heap and then places `x` into it.
    ///
    /// # Examples
    ///
    /// ```
    /// let five = Box::new(5);
    /// ```
    #[stable(feature = "rust1", since = "1.0.0")]
    #[inline(always)]
    pub fn new(x: T) -> Box<T> {
        box x
    }
}

と動作するのであろうと考えられる. だから, 冒頭の説明は, moveしてるからコピーしてないと考えることが出来る.

2について, Boxのderefは以下のように実装されていて,

#[stable(feature = "rust1", since = "1.0.0")]
impl<T: ?Sized> Deref for Box<T> {
    type Target = T;

    fn deref(&self) -> &T {
        &**self
    }
}

これは,

  1. Boxのimmutable borrowを受け取る
  2. それをderefする -> Boxになる
  3. さらにderefする -> ここでmove?
  4. &でそれをborrowする (WHY!!! Rust WHY!!! なぜ3の段階でreturnしない!!!)

と読めるような気がする.

全体的に確証は持てないが, 教訓は,

  1. Cのpointer返しのような要領でBox返しはやめよう. 値をそのまま返してもコピーは起きない
  2. Box返しする時は抽象を返す時だ (ファクトリパターン. moveクロージャを返すとか)
  3. それだけ守っていれば, あとはRustが一番良くしてくれる!!!(であろう)

ですね!