はじめに
Rustのコンストラクタで次のようなコードを書いたがいくつか疑問が浮かんだ。
# [derive(Debug, Copy, Clone)]
struct Hoge {
value: i32,
}
# [derive(Debug)]
struct HogeList {
vec: Vec<Hoge>,
}
impl HogeList {
fn new(val: i32, size: usize) -> HogeList {
HogeList {
vec: vec![Hoge { value: val }; size],
}
}
}
fn main() {
let hogeList = HogeList::new(3);
println!("{:?}", hogeList);
}
疑問
- HogeList::new()内で作られたVecはいつ解放されるのか?
- main()内のhogeList.vecと上記のnew()で作られたものは同一 or 同じ値の別物?
- 大容量のVecをコンストラクタで作ってしまって大丈夫?
ここに書いてあることが理解できればわざわざ確認したくて良いのだが、読んだだけだとよくわからなかったので実際に確かめてみた。
結論
- Vecの所有者がスコープから抜ける時に中身も解放される
- 関数の戻り値や変数束縛で、中身へのポインタは新しく作られるが、保持している中身は同じもの
- 中身がコピーされるわけではないので、大容量のVecを作って関数の戻り値に指定しても問題なさそう
確認用コード
アドレスとdropタイミングがわかるようなコードで確かめてみた。
# [derive(Debug, Clone)]
struct Hoge {
value: i32,
}
impl Drop for Hoge {
fn drop(&mut self) {
println!("drop Hoge {}", self.value);
}
}
# [derive(Debug)]
struct HogeList {
vec: Vec<Hoge>,
}
impl HogeList {
fn new(val: i32, size: usize) -> HogeList {
let vec = vec![Hoge { value: val }; size];
let p_vec: *const Vec<Hoge> = &vec;
let p_val: *const Hoge = &vec[0];
println!("(new{}) vec address: {:?}", val, p_vec);
println!("(new{}) val address: {:?}", val, p_val);
HogeList {
vec: vec,
}
} // vecはスコープから抜けるが所有権はnew()の呼び出し元に移動されるので、中身は解放されない
}
fn main() {
println!("[main scope begin]");
let hoge_list1 = HogeList::new(1, 1);
println!("hoge_list1: {:?}", hoge_list1);
let p_vec: *const Vec<Hoge> = &hoge_list1.vec; // new()とmain()のVecは別物
let p_val: *const Hoge = &hoge_list1.vec[0]; // 中身のアドレスは同じ
println!("(main) vec address: {:?}", p_vec);
println!("(main) val address: {:?}", p_val);
let hoge_list2 = HogeList::new(2, 1);
println!("hoge_list2: {:?}", hoge_list2);
{
println!("[inner scope begin]");
let hoge_list3 = hoge_list1; // hoge_list1.vecの所有者がhoge_list3.vecに移動する
let p_vec: *const Vec<Hoge> = &hoge_list3.vec;
let p_val: *const Hoge = &hoge_list3.vec[0];
println!("(inner scope) vec address: {:?}", p_vec); // Vecは別物
println!("(inner scope) val address: {:?}", p_val); // 中身は同じ
println!("[inner scope end]");
} // hoge_list3が所有しているVecの中身<Hoge(1)>はここで解放される
println!("[main scope end]");
} // hoge_list2が所有しているVecの中身<Hoge(2)>はここで解放される
結果
[main scope begin]
(new1) vec address: 0x7ffd18340750 # new()で作られたVec
(new1) val address: 0x558b600feb80 # Vecが保持するHoge(1)のアドレス
hoge_list1: HogeList { vec: [Hoge { value: 1 }] }
(main) vec address: 0x7ffd18340970 # hoge_list1とnew()のVecは別アドレス
(main) val address: 0x558b600feb80 # Vecは別物でも保持する中身Hoge(1)は同じ
(new2) vec address: 0x7ffd18340750 # (本記事から話がそれるが、スタックのアドレスは使いまわされる)
(new2) val address: 0x558b600feba0 # 新しく作られたHoge(2)のアドレス
hoge_list2: HogeList { vec: [Hoge { value: 2 }] }
[inner scope begin]
(inner scope) vec address: 0x7ffd18340b20 # hoge_list3.vecのアドレス
(inner scope) val address: 0x558b600feb80 # 中身は同じ
[inner scope end]
drop Hoge 1 # Vec[Hoge(1)]の所有者hoge_list3がスコープから抜けるのでHoge(1)が解放される
[main scope end]
drop Hoge 2 # Vec[Hoge(2)]の所有者hoge_list2がスコープから抜けるのでHoge(2)が解放される
イメージとしてはこんな感じ
Vecを変数束縛する、もしくは関数の戻り値として返すと中身(上の図のvalue)へのポインタを保持する別のVecが作られて、そちらからしかvalueにアクセスできなくなる(これを所有権の移動=ムーブという)。
新しい方のVecがスコープから外れてだれも中身を管理しなくなると中身の方もヒープから取り除かれる。
結論(再掲)
- Vecの所有者がスコープから抜ける時に中身も解放される
- Vecが関数の戻り値で返されたりや変数束縛されるとムーブになるので、所有者は変わっているが保持している中身は同一
- 中身がコピーされるわけではないので、大容量のVecを作って関数の戻り値に指定しても問題ない
更新記録
4/8 コメントの指摘を受けて「コピー」の文言を修正