🦀

Rustのstd::mem::replaceの使い方

に公開

背景

このドキュメントに沿ってRustで単連結リストを書いていて、リストに要素を追加する際、既存のリストの先頭要素を新しく追加する要素のnextフィールドに付け替えようとしたところ、以下のようなエラーが発生した。

cannot move out of self.head which is behind a mutable reference

use std::mem;

pub struct List {
    head: Link,
}

type Link = Option<Box<Node>>;

struct Node {
    elem: i32,
    next: Link,
}

impl List {
    pub fn new() -> Self {
        Self { head: None }
    }

    // `List` の先頭に `Node` を追加する。
    pub fn push(&mut self, elem: i32) {
        let node = Box::new(Node {
            elem,
            // cannot move out of `self.head` which is behind a mutable reference
            next: self.head,
        });

        self.head = Some(node);
    }
}

エラーメッセージによると、可変参照(&mut)を通じての所有権移動が許可されていないということらしい。
要するに、参照であるselfからその要素をmoveすることはできないということである。
self.head.clone()としても良いが、このようなケースにおいて一般に対象がcloneを実装しているとは限らないので、いつでもcloneできるとは限らない。

本題

これはよく知られたエラーらしく、std::mem::replaceを使うことで解決できる。

pub fn push(&mut self, elem: i32) {
    let node = Box::new(Node {
        elem,
        next: mem::replace(&mut self.head, None),
    });

    self.head = Some(node);
}

replaceは以下のようなシグネチャを持っており、destの値をsrcで書き換え、書き換える前のdestの値を返す関数である。

pub fn replace<T>(dest: &mut T, src: T) -> T

replaceの実装を見てみよう。非常にシンプルで、説明の余地はほとんどなさそうである。

pub const fn replace<T>(dest: &mut T, src: T) -> T {
    // SAFETY: We read from `dest` but directly write `src` into it afterwards,
    // such that the old value is not duplicated. Nothing is dropped and
    // nothing here can panic.
    unsafe {
        let result = ptr::read(dest);
        ptr::write(dest, src);
        result
    }
}

唯一、戻り値の所有権について気になるところだが、 ptr::readのドキュメントによると、ptr::read<T>

read creates a bitwise copy of T

と書いてあるので、readの戻り値は所有権付きであることがわかる。

ちなみに、read(&s)sと同一のメモリを指すため、readの戻り値とsを同時に使用するのは危険である旨が書かれている。
具体的には、let s2 = read(&s)とした後にs2に他の値を再代入すると、根底にあるメモリはdropされてしまうため、それ以降sに再代入しようとすると根底にあるメモリに対して再びdropが呼ばれることになり、未定義動作を引き起こす。

ちなみにちなみに、今回の例のようにOptionを使う場合には、takeというラッパー関数が使える。

pub const fn take(&mut self) -> Option<T> {
      // FIXME replace `mem::replace` by `mem::take` when the latter is const ready
      mem::replace(self, None)
}
pub fn push(&mut self, elem: i32) {
    let node = Box::new(Node {
        elem,
        next: self.head.take(),
    });

    self.head = Some(node);
}
GitHubで編集を提案

Discussion