Rustコトハジメ

プログラミング言語Rustと競プロに関する情報をお届けします。

Indexトレイトをしゃぶり尽くす

fn main() {
    let mut v = vec![1, 2, 3, 4, 5];
    let a = &v[0];
    v.push(6);
}

このプログラムをコンパイルすると、

error[E0502]: cannot borrow `v` as mutable because it is also borrowed as immutable
 --> prog.rs:4:5
  |
3 |     let a = &v[0];
  |              - immutable borrow occurs here
4 |     v.push(6);
  |     ^ mutable borrow occurs here
5 | }
  | - immutable borrow ends here

というエラーによって拒絶される。こんな基本中の基本に見える小さなプログラムから多くのことを学べてしまったので共有したい。

(&v)[0]なのか&(v[0])なのか?

私がはじめに確認したのは、aが&i32なのは良いとして、一体それがどうやって導出されたのかだ。Indexの定義は以下である。

pub trait Index<Idx: ?Sized> {
    type Output: ?Sized;
    fn index(&self, index: Idx) -> &Self::Output;
}

参照をとって、参照を返す。Rustを知ると、これがしっくり来ることが分かる。ライフタイムをつけると、fn index(&'a self, index: Idx) -> &'a Self::Outputだからだ。これは、「Outputはセルフからの借用だから、人生を共にする」という意味であり、正気に見える。

後に説明するように、[i]シンタックスシュガーなのだが、私はこれを知らなかったから、てっきり(&v)[0]なのだと思っていたのだが、もしそうであればそもそもauto-referencingがあるのだからv[0]と書けばよいのであって、なお混乱する。

結局、これは&(v[0])であるが正解なのだが、まずはこの問題をすばやく解決するために実験で確かめよう。そしてその実験がさらに疑問を生むのだが・・・。

(&v)[0]と書き換える

なんと、こう書くとコンパイルが通ってしまう。

&(v[0])と書き換える

こちらはさきほどと同じエラーを出す。したがって、&v[0]&(v[0])の意味である。で決着する。

考察

ドキュメントに、

container[index] is actually syntactic sugar for *container.index(index)

と書いてあるように、v[i]*v.index(i)シンタックスシュガーである。つまり、v.index(i)の返り値は&Self::Outputだから、v[i]Self::Outputとなる。&v[i]というのは、その参照をとっているから&i32なのだ。

一方、(&v)[0]はどうか。これは、i32となる。

まとめると、

  • v.index(i)&Self::Outputである
  • *v.index(i)Self::Outputである // これが(&v)[0]
  • &(*v.index(i))&Self::Outputである // これが&v[0]

となる。

参照をderefするとrvalueとして所有者を得る

ではこの中間のSelf::Outputとは一体何なのか。なぜ、(&v)[0]のバージョンはコンパイルが通るのか?「参照をとったものの値を覗いてる」のだから「参照をとっているようなもの」と考えることも出来て、同じく「borrowしてるからmutable borrow出来ない」と怒られても不思議ではないのだが。

結論をいうと、直感のとおり、参照に対してderefすると、その所有者を得る。そして、(&v)[0]バージョンでは、これをaにCopyしている。同じような疑問に基づくスレッドを見つけたので参考までにリンクしておく。

Why does dereferencing here involve a move? : rust

所有者を得ている

プログラムをCopyのi32からStringに変更すると、エラーになる。

fn main() {
    let mut v = vec!["a".to_string(), "b".to_string()];
    let a: String = (&v)[0];
    v.push("c".to_string());
}
error[E0507]: cannot move out of indexed content
 --> prog.rs:3:21
  |
3 |     let a: String = (&v)[0];
  |                     ^^^^^^^
  |                     |
  |                     cannot move out of indexed content
  |                     help: consider using a reference instead: `&(&v)[0]`

これはまさに、vの中から要素の所有権をMoveしようとしたことを意味する。

&&*&は別ものであることの確認

一方、&(v[0])バージョンでは、所有者を得たあとにやっぱり参照をとっている。これが一体何をしているのか確認する。

fn main() {
    let x: i32 = 1;
    let p0: &i32 = &x;
    let p1: &i32 = &(*(&x));
    println!("xp={:p},p0={:p},p1={:p}", &x, &p0, &p1);
}

このプログラムを実行すると、

xp=0x7fff9fef08c4,p0=0x7fff9fef08c8,p1=0x7fff9fef08d0

が出力される。この意味を図にしてみると、

f:id:akiradeveloper529:20190101152648j:plain

となる。

実際の&v[0]のケースでは、p0に相当するファットポインタも、そこからderefして得られる所有者もrvalueのまま変数束縛されずに捨てられるからアドレスは得られないが、実際にはこのようなことを行ってると考えても、概念的には等しい。