W.I.S. Laboratory
menu-bar

Rust


Rustの借用はポインタ渡しか参照渡しか

Rustは基本ユニークポインタを使うので、ポインタを他の変数に代入したり関数の引数として渡したりすると所有権が移動してしまう。
RefCellをRcやArcでラップすれば参照カウント式のスマートポインタとして使えるが、いろいろと面倒くさい。
なので通常は借用を使ってなんとかするわけだ。
Rustでは変数の前に「&」や「&mut」を付けることで変数の参照を関数に渡すことができ、これを「借用」と呼ぶ。
「借用=ポインタ渡し」だと思っていたのだが、どうもそうとは限らないらしい。
なので備忘録。

まずC。

なんということもない。いたって普通だ。
変数のアドレスを渡すときはポインタ渡し、mallocしたアドレスを渡すときはポインタを値渡しする。
C++のスマートポインタ(ユニークポインタ)だと

スマートポインタを参照渡しする。(関数側でスマートポインタのエイリアスを受け取る)
続いてRust。

渡す側はCと同じなので、ほぼ混乱はない。
問題は受け取る側だ。
Cだと int *a としていたところを Rustでは a: &i32 と書く。
Cは「int型のポインタ変数」という書き方だが、Rustは「i32が格納されたアドレス値」という感じの書き方だ。
変数名はプリフィックス無しのそのままで、型名に借用の意味である「&」を付ける。
Cと同じく、変数名の前に「&」は不要だ。
受け取った側でデリファレンスできるので、これはポインタ渡しだ。
ここで混乱が生じる原因のひとつに、println!マクロの引数の書き方がある。

デリファレンスしてもしなくても、結果が同じなのだ。
Cを書いてきた人が混乱すること請け合いだ。
アドレス値を出力するには、

こうするらしい。
初見殺しっぷりが凄まじい。

ややこしいのがミュータブル借用だ。
Cだと万年ミュータブルなので息を吐くようにミュータブル借用できるのだが、Rustはそうもいかない。

突然あちこちに「mut」が増える。
借用する変数自体をミュータブルにし、関数に渡すときにも「&mut」が必要だ。
「なんで?宣言時に let mut してるやんけ」と思うのだが、これは &(mut a) ではなく (&mut) a らしい。
ミュータブル識別子には mut(値のミュータブル) と &mut(借用先のミュータブル) がある。
なのでここは「この引数は指し示す先の値を書き換えても良いアドレス値だよ」と関数に渡している。
受け取る側も同じで a: &mut i32 という具合に「&mut」を型名のプリフィックスとして付与すると「メモリ内容を書き換えても良いi32が格納されたアドレス値ですね」として受け取ることができる。
このとき、mut a: &mut i32 とする必要はない。
ミュータブルなのはあくまでデリファレンス時の値であって、アドレス値そのものではないからだ。
*a += 1 のデリファレンスを外して a += 1 とすると「ポインタに対して演算はできないよ」と怒られる。
ということでこれもポインタ渡しだ。

ではBox化するとどうなるのか。
RustではBox化するとそのデータはヒープに置かれ、変数にはそのアドレス値が入る。
そしてこの変数こそスマートポインタ(ユニークポインタ)だ。
C++でいうところの std::make_unique したものと思って差し支えない。

なんと書き方が同じなのだ。
変数 a にはi32の10が入っているメモリのアドレスが入っている。(構造体なので他にもいろいろ入っているが)
つまり a はポインタなのだが、にも関わらずその a に対して mut 宣言が必要だ。
これは「変数 a がミュータブルだよ」という意味だけではなく、「変数 a も、a が指し示す先にあるメモリ内容も全部ミュータブルだよ」という意味になるようだ。
しかしCを書いてきた人の頭はこれを「アドレス値を書き換えても良いポインタ変数だよ」と思ってしまう。
そしてその変数 a を &mut で、つまりミュータブル借用で渡しているのだから、「アドレス値が格納された変数が置かれたメモリアドレス、つまりユニークポインタへのポインタを渡している」と読んでしまう。
なのだが、実際RustではユニークポインタがC++のような参照渡しで渡されているようだ。(もしこれがポインタ渡しなら2重デリファレンス(**a)できるはずなのだが、それをすると cannot be dereferenced エラーになる)
つまり、上のp関数で受け取っているポインタaは、main関数内でlet mutしたポインタaそのものを参照しているだけ(つまりエイリアス)なのだ。
Rustが「ポインタ渡し」とか「参照渡し」と呼ばず敢えて「借用」と呼んでいるのも、コード上の記述がまったく同じなのにポインタ渡しをする場合と参照渡しをする場合があるからではないだろうか。
そしてそれは記述上の「値」と「アドレス」の区別の緩さに繋がっている。
上に書いた println! マクロの引数をデリファレンスしてもしなくても同じ結果になるというのも、「値」と「アドレス」の区別を緩く扱っているからといえそうだ。
少し話が逸れるが、Rustで「Boxの中身はミュータブルだが、そのポインタ変数はイミュータブルにしたい(JavaScriptでいう const a = {x: y}; の状態)」というときは、全フィールドをcellで囲うほか無いようだ。

さらに借用の書き方の記憶が混乱する原因に、構造体にインプリメンテーションする関数の第一引数の書き方も関係しているかもしれない。

第一引数は &self なのだが、この書き方が他の引数と違う。
構造体のポインタを受け取っているにもかかわらず、変数名の前に「&」がついているかのように見える。
頻繁に書いているうちに「これがRustの借用の書き方なのかぁ」となんとなく思ってしまうようだ。
しかしこの書き方が特殊なのであって、一般的なRustの借用の書き方ではない。
これは self: &self のシンタックスシュガーらしい。

・・素直に丸暗記してしまったほうが近道のような気もするのだが、それが苦手な性格なので

  • 参照記号「&」は同じ記述なのにポインタ渡しをしたり参照渡しをしたりする
  • 記述上の値とアドレスの区別が緩い
  • 構造体にインプリメンテーションする関数の第一引数の書き方は特殊
このように理解しておこうと思う。


[ 戻る ]
saluteweb