yohhoyの日記

技術的メモをしていきたい日記

ミュータブル参照のムーブと再借用

Rust言語のミュータブル参照(&mut T)の振る舞いについてメモ。[本記事はRust 1.10/Stable準拠]

ミュータブル参照m1から新しいミュータブル参照m2を変数束縛(variable binding)するとき、型の明示有無によって(a)ミュータブル参照の所有権が移動(ムーブ)するケースと、(b)ミュータブル参照の再借用(reborrow)が行われるケースが存在する。

// m2の変数束縛で型推論
let mut x: i32 = 0;
let m1: &mut i32 = &mut x;
{
  let m2 = m1;  // (a1) m1からm2へムーブ
  *m2 = 42;
}
assert!(*m1 == 42);  // (a2) NG: m1は既に所有権を持たない
// error: use of moved value: `*m1`
// note: `m1` moved here because it has type `&mut i32`, which is moved by default
//  let m2 = m1;
// m2の変数束縛で型(&mut i32)を明示
let mut x: i32 = 0;
let m1: &mut i32 = &mut x;
{
  let m2: &mut i32 = m1;  // (b1) m2からm1へ再借用
  *m2 = 42;
//assert!(*m1 == 42);  // error: cannot use `*m1` because it was mutably borrowed
}  // (b1') m2の借用期間おわり
assert!(*m1 == 42);  // (b2) OK: 所有権はm1に残ったまま

いずれのケースでもm2の型は&mut i32に相当するが、厳密には参照のライフタイム(lifetime)が異なる別の型である。つまりlet束縛における&mut i32という型表記ではライフタイムが省略されており、Rust言語における具象型ではないために起きている。次の疑似コード例では*1m2が参照する先のライフタイムが異なる(&'a mut i32&'b mut i32)。

// m2の変数束縛で型推論
{ // ライフタイムaの開始
  let mut x: i32 = 0;
  let m1: &'a mut i32 = &mut x;  // m1の厳密な型は &'a mut i32
  {
    let m2 = m1;  // (a1) m2は &'a mut i32 に型推論される
    // m1とm2は同じ &'a mut i32 型のため所有権がムーブされる
    *m2 = 42;
  }
  // m1は既に所有権を持たない
  assert!(*m1 == 42);  // (a2) NG
} // ライフタイムaの終了
// m2の変数束縛で型(&mut i32)を明示
{ // ライフタイムaの開始
  let mut x: i32 = 0;
  let m1: &'a mut i32 = &mut x;  // m1の厳密な型は &'a mut i32
  { // ライフタイムbの開始
    let m2: &mut i32 = m1;  // (b1) m2は &'b mut i32に型推論される
    // m2はm1より狭いライフタイムを参照するため再借用が行われる
    *m2 = 42;
  } // ライフタイムbの終了
  // m2のスコープを抜けて借用はm1に返される
  assert!(*m1 == 42);  // (b2) OK
} // ライフタイムaの終了

前掲ルールは、ジェネリック関数におけるパラメータ記述方法の違いによっても顕在化する。ミュータブル参照をジェネリックパラメータTで単純に受け取ると(1)、関数呼び出しに指定した変数束縛v1はムーブにより所有権を失う。

#[derive(Debug)]
struct S;

// 型パラメータT による T型
fn sink1<T>(_: T) {}
// ライフタイムパラメータ'a と 型パラメータT による &'a mut T型
fn sink2<'a,T>(_: &'a mut T) {}
// sink2と同義:入力ライフタイムパラメータを省略した記法
fn sink3<T>(_: &mut T) {}

fn main() {
  let (v1, v2, v3) = (&mut S, &mut S, &mut S);

  // (1) Tはv1と同じライフタイムのミュータブル参照型に推論される
  sink1(v1);
//print!("{:?}", v1);  // error: use of moved value: `v1`

  // (2) 'aは関数スコープ相当のライフタイムを表し TはS型へと推論されるため
  //     &'a mut T型はv2と異なるライフタイムのミュータブル参照型となる
  sink2(v2);
  print!("{:?}", v2);  // OK

  // (3) (2)と同じ型推論が行われる
  sink3(v3);
  print!("{:?}", v3);  // OK
}

関連URL

*1:プログラム全域を指す特殊なライフタイム 'static を除き、Rust言語の構文上はlet変数束縛で参照のライフタイムを明示できない。