Rustの参照、Box、Rcを関数の引数・返り値にした場合の挙動

公開:2020/10/08
更新:2020/10/12
6 min読了の目安(約6100字TECH技術記事

適当な構造体を作って、関数の引数や返り値としてその構造体やBoxを指定した場合の挙動を確認してみる。

用意した構造体

アドレスを確認するために、i32のメンバを持つObjCopyトレイトを実装するObjCpを用意した。

use std::fmt::Debug;
use std::rc::Rc;

trait New { fn new(x: i32) -> Self; }
trait Ptr { fn ptr(&self) -> *const i32; }
trait Add { fn add(&mut self, value: i32); }

#[derive(Debug)]
struct Obj { x: i32, }
impl Ptr for Obj {
    fn ptr(&self) -> *const i32 {
        &self.x
    }
}
impl Add for Obj {
    fn add(&mut self, value: i32) {
        self.x += value;
    }
}
impl New for Obj {
    fn new(x: i32) -> Self {
        Obj { x: x }
    }
}

#[derive(Debug, Copy, Clone)]
struct ObjCp { x: i32, }
impl New for ObjCp {
    fn new(x: i32) -> Self {
        ObjCp { x: x }
    }
}
impl Ptr for ObjCp {
    fn ptr(&self) -> *const i32 {
        &self.x
    }
}
impl Add for ObjCp {
    fn add(&mut self, value: i32) {
        self.x += value;
    }
}

関数の引数

値渡し

fn wo_ref<T: Debug + Ptr + Add>(mut a: T) {
    a.add(1);
    println!("{:?}, addr={:?}", a, a.ptr());
}
println!("値渡し (Copyトレイトなし)");
let a = Obj { x: 0 };
println!("[caller] addr={:?}", a.ptr());
wo_ref(a); // ここで所有権が移動されるので、以降はaを使えない
// wo_ref(a); // 移動後の使用なのでエラー
println!();

println!("値渡し (Copyトレイトあり)");
let a = ObjCp { x: 0 };
println!("[caller] addr={:?}", a.ptr());
wo_ref(a); // コピーが発生して、それがwo_ref内部で使われる
wo_ref(a); // コピーが毎回作られるので、何回でも使える
println!("[caller] {:?}, addr={:?}", a, a.ptr());

実行結果

値渡し (Copyトレイトなし)
[caller] addr=0x7ffedb8dc26c
Obj { x: 1 }, addr=0x7ffedb8dbf7c # 呼び出し元とアドレスが異なる

値渡し (Copyトレイトあり)
[caller] addr=0x7ffedb8dc32c
ObjCp { x: 1 }, addr=0x7ffedb8dbf7c # 呼び出し元とアドレスが異なる
ObjCp { x: 1 }, addr=0x7ffedb8dbf7c # 呼び出し元とアドレスが異なる (一つ上と同じアドレスだが、スコープが閉じたあとに再利用されているだけで別物)
[caller] ObjCp { x: 0 }, addr=0x7ffedb8dc32c # 関数内の変更は反映されない

値渡しの場合、Copyトレイトの有無によらず、関数の呼び出し元と関数内部でアドレスが異なる=新しいインスタンスが作られている。
そのため、関数内の変更は呼び出し元には反映されない。
Copyトレイトを実装しない場合は所有権の移動になり、あとで使えなくなるので、基本的には参照を渡すようにする。

借用

fn with_ref<T: Debug + Ptr + Add>(a: &mut T) {
    (*a).add(1);
    println!("{:?}, addr={:?}", *a, (*a).ptr());
}
println!("参照を引数に指定(借用)");
let mut a = ObjCp { x: 0 };
println!("[caller] addr={:?}", a.ptr());
with_ref(&mut a); // 借用
with_ref(&mut a); // 借用
println!("[caller] {:?}, addr={:?}", a, a.ptr());

実行結果

参照を引数に指定(借用)
[caller] addr=0x7ffedb8dc464
ObjCp { x: 1 }, addr=0x7ffedb8dc464 # 呼び出し元と同じアドレス
ObjCp { x: 2 }, addr=0x7ffedb8dc464 # 呼び出し元と同じアドレス
[caller] ObjCp { x: 2 }, addr=0x7ffedb8dc464 # 関数内での変更が反映される

参照渡しの場合、関数内で操作しているものは呼び出し元と同じものなので変更が反映される。

Box、Boxの参照を引数として指定

fn with_box<T: Debug + Ptr + Add>(mut a: Box<T>) {
    (*a).add(1);
    println!("{:?}, addr={:?}", *a, (*a).ptr());
}
fn with_refbox<T: Debug + Ptr + Add>(a: &mut Box<T>) {
    (**a).add(1);
    println!("{:?}, addr={:?}", **a, (**a).ptr());
}
println!("Boxを引数に指定");
let a = Box::new(ObjCp { x: 0 });
println!("[caller] addr={:?}", a.ptr());
with_box(a); // ここで所有権が移動されるので、以降はcを使えない
// with_box(a); // 移動後の使用なのでエラー
println!();

println!("Boxの参照を引数に指定");
let mut a = Box::new(ObjCp { x: 0 });
println!("[caller] addr={:?}", (*a).ptr());
with_refbox(&mut a); // Boxの借用
with_refbox(&mut a); // Boxの借用
with_refbox(&mut a.clone()); // cloneはインスタンスを複製する
println!("[caller] {:?}, addr={:?}", (*a), (*a).ptr());

実行結果

Boxを引数に指定
[caller] addr=0x5579cc0e3c80 # ヒープ領域に割当
ObjCp { x: 1 }, addr=0x5579cc0e3c80 # 呼び出し元と同じアドレス

Boxの参照を引数に指定
[caller] addr=0x5579cc0e3c80 # 一つ前のインスタンスは解放されているので、アドレスが再利用されていると思われる
ObjCp { x: 1 }, addr=0x5579cc0e3c80 # 呼び出し元と同じアドレス
ObjCp { x: 2 }, addr=0x5579cc0e3c80 # 呼び出し元と同じアドレス
ObjCp { x: 3 }, addr=0x5579cc0e3ca0 # 複製されたインスタンスなので別アドレス
[caller] ObjCp { x: 2 }, addr=0x5579cc0e3c80 # 関数内での変更が反映される(最後のものは複製への変更なので複製元は変更されない)

BoxとRcはヒープ領域にメモリが割り当てられる。

Box自体はCopyでない構造体なので、引数に渡すと所有権が移動してしまい、あとで使えなくなる。
Boxの参照を渡すようにすればこれを回避できるが二重ポインタのようになる(*は省略できるので読みにくくなるわけではない)。
中身の構造体がCloneを実装していれば、clone()で複製することができる。この場合deep copyになり、Rcと挙動が異なる。

Rcを引数に指定

fn with_rc<T: Debug + Ptr + Add>(a: Rc<T>) {
    // (*a).set(1); // RcはDerefMutを実装していないのでエラー
    println!("{:?}, addr={:?}", *a, (*a).ptr());
}
println!("Rcを引数に指定");
let a = Rc::new(ObjCp { x: 0 });
println!("[caller] addr={:?}", a.ptr());
with_rc(a.clone()); // Rcのクローンで参照カウンタを増やすが、ObjCpそのものがコピーされるわけではない
// with_rc(a); // こう書くとRcの所有権が移動されて、以降は使えない
println!("[caller] {:?}, addr={:?}", a, a.ptr());

実行結果

Rcを引数に指定
[caller] addr=0x5579cc0e3cb0 # ヒープ領域に割当
ObjCp { x: 0 }, addr=0x5579cc0e3cb0 # 呼び出し元と同じアドレス
[caller] ObjCp { x: 0 }, addr=0x5579cc0e3cb0

RcはもBoxと同様にヒープに割り当てられる。
Boxとは異なり複数の所有権が認められており、clone()でshallow copyされる。すなわち、中身が複製される(=ObjCpの新しいインスタンスが作られる)わけではなく、同じインスタンスの所有者が増える。
mutableにできないので、内部の値を変更したい場合はRc<Cell<T>>Rc<RefCell<T>>>を使う。

関数の返り値

fn return_val<T: Debug + New + Ptr>() -> T {
    let r = T::new(0);
    println!("{:?}, addr={:?}", r, r.ptr());
    r
}
// ローカル変数の参照を返すことはできない
// fn return_ref<T: Debug + New + Ptr>() -> &'static T {
//     let r = T::new(0);
//     println!("{:?}, addr={:?}", r, r.ptr());
//     &r
// }
fn return_box<T: Debug + New + Ptr>() -> Box<T> {
    let r = Box::new(T::new(0));
    println!("{:?}, addr={:?}", *r, (*r).ptr());
    r
}
fn return_rc<T: Debug + New + Ptr>() -> Rc<T> {
    let r = Rc::new(T::new(0));
    println!("{:?}, addr={:?}", *r, (*r).ptr());
    r
}
println!("値を返す");
let a = return_val::<Obj>();
println!("[caller] {:?}, addr={:?}", a, a.ptr());

println!("Boxを返す");
let a = return_box::<Obj>();
println!("[caller] {:?}, addr={:?}", *a, (*a).ptr());

println!("Rcを返す");
let a = return_rc::<Obj>();
println!("[caller] {:?}, addr={:?}", *a, (*a).ptr());

実行結果

値を返す
Obj { x: 0 }, addr=0x7ffc474d6a0c
[caller] Obj { x: 0 }, addr=0x7ffc474d6bcc # 生成時とアドレスは異なる

Boxを返す
Obj { x: 0 }, addr=0x556b8e1d4c80
[caller] Obj { x: 0 }, addr=0x556b8e1d4c80 # 同じアドレス

Rcを返す
Obj { x: 0 }, addr=0x556b8e1d4cb0
[caller] Obj { x: 0 }, addr=0x556b8e1d4cb0 # 同じアドレス

関数で返り値を返すときもムーブセマンティクスになる。
単純な構造体では新しいインスタンスが生成されているが、Box、Rcはヒープに作られたインスタンスをそのまま引き継げる。
関数内でインスタンスを作ってその参照を返すといったことは、そのインスタンスが関数の終了とともに破棄されるのでできない。