[Rust] Drop/dropあれこれ
Rustではstd::ops::Drop
というトレイトを使うことにより、C++と同様デストラクタを定義できます.デストラクタが定義できると、RAII (Resource Acquisition Is Initialization)というパターンが使え、これにより以下のような処理を確実に[1]行わせることができます.
- 確保したメモリを開放する
- 開いたファイルを閉じる
- 確保したMutex等のロックを開放する
例えば、C++だと以下のように書くところ、
#include <iostream>
#include <string>
class Sample {
public:
explicit Sample(std::string name) : name_(name) {
std::cout << "ctor " << name_ << std::endl;
}
~Sample() {
std::cout << "dtor " << name_ << std::endl;
}
private:
std::string name_;
};
int main() {
Sample a("A");
{
Sample b("B");
}
std::cout << "-----" << std::endl;
}
ctor A
ctor B
dtor B
-----
dtor A
Rustだと以下のように書くことで同様の振る舞いになります.
#![allow(unused_variables)]
struct Sample {
name: String
}
impl Sample {
fn new(name: &str) -> Self {
println!("ctor {}", name);
Self { name: name.to_string() }
}
}
impl Drop for Sample {
fn drop(&mut self) {
println!("dtor {}", self.name);
}
}
fn main() {
let a = Sample::new("A");
{
let b = Sample::new("B");
}
println!("-----");
}
ctor A
ctor B
dtor B
-----
dtor A
Drop
トレイトは唯一の関数fn drop(&mut self)
を持っており、Rustでは値がスコープから抜けると、Drop::drop
が内部的に呼び出されます.ここではb
の方がa
よりもライフタイムが短いため、dtor A
より先にdtor B
が出力されています.
普通に使うための理解としてはおそらくこれで十分で、実際Rust by ExampleやTRPLにはそれくらい(TRPLはstd::mem::drop
の話もあるが)の内容しか書かれていません.
最近、個人的にFFIとかしている中で、もう少し詳しく知りたくなったので色々まとめてみましたという記事です.
入れ子の場合の呼び出し順
一番知りたかった内容ですが、Drop
を定義した型が入れ子になった場合に呼び出し順はどうなるのでしょうか.
公式リファレンスに以下のような記述があります.
The destructor of a type
T
consists of:
- If
T: Drop
, calling<T as std::ops::Drop>::drop
- Recursively running the destructor of all of its fields.
- The fields of a struct are dropped in declaration order.
- The fields of the active enum variant are dropped in declaration order.
- The fields of a tuple are dropped in order.
- The elements of an array or owned slice are dropped from the first element to the last.
- The variables that a closure captures by move are dropped in an unspecified order.
- Trait objects run the destructor of the underlying type.
- Other types don't result in any further drops.
引用元:https://doc.rust-lang.org/reference/destructors.html
訳してみます.
T
型のデストラクタは以下からなる
-
T: Drop
を満たしていれば、<T as std::ops::Drop>::drop
を呼ぶ - 全てのフィールドのデストラクタを再帰的に実行する
- 構造体の場合、フィールドは宣言順にdropされる
- enumの場合、フィールドは宣言順にdropされる
- タプルの場合、フィールドは順番通りdropされる
- 配列や所有しているスライスの要素は先頭からdropされる
- クロージャがムーブキャプチャした変数のdrop順序は未指定
- トレイトオブジェクトではunderlying type(後述)のデストラクタが実行される
- その他の型はそれ以上の処理を行わない(つまり1のみということ)
ということで、実際にコードを使って振る舞いを見てみます.
#![allow(dead_code)]
struct Parent {
a: Child,
b: Child,
}
impl Parent {
fn new() -> Self {
Self {
a: Child::new("a"),
b: Child::new("b"),
}
}
}
impl Drop for Parent {
fn drop(&mut self) {
println!("drop Parent");
}
}
struct Child {
prefix: String,
a: GrandChild,
b: GrandChild,
}
impl Child {
fn new(prefix: &str) -> Self {
Self {
prefix: prefix.to_string(),
a: GrandChild { msg: format!("{}_a", prefix) },
b: GrandChild { msg: format!("{}_b", prefix) },
}
}
}
impl Drop for Child {
fn drop(&mut self) {
println!("drop Child: {}", self.prefix);
}
}
struct GrandChild {
msg: String,
}
impl Drop for GrandChild {
fn drop(&mut self) {
println!("drop GrandChild: {}", self.msg);
}
}
fn main() {
Parent::new();
}
drop Parent
drop Child: a
drop GrandChild: a_a
drop GrandChild: a_b
drop Child: b
drop GrandChild: b_a
drop GrandChild: b_b
以下の記述で1, 2が順番通り処理されるとしたときに想定される通りの順番になっていることが確認できました.
-
T: Drop
を満たしていれば、<T as std::ops::Drop>::drop
を呼ぶ - 全てのフィールドのデストラクタを再帰的に実行する
- 構造体の場合、フィールドは宣言順にdropされる
トレイトオブジェクトのデストラクタ
先のThe Rust Referenceのデストラクタの説明で、トレイトオブジェクトの箇所だけ少し引っかかったので、こちらも詳しく見てみます.
Trait objects run the destructor of the underlying type.
引用元:https://doc.rust-lang.org/reference/destructors.html
underlying typeというとC++のstd::underlying_type
のように、基底型という訳が見つりますが、今回は少し違う気がします.
Slackのrust-jpグループ[2] で尋ねたところ、underlyingの元々の意味「隠された・裏にある・潜在的な」として解釈した方がよいというような回答をいただきました.
ということで、ここではトレイトオブジェクトの裏にある元の具象型というような意味合いのようです.
#[allow(unused_variables)]
struct Concrete;
impl Drop for Concrete {
fn drop(&mut self) {
println!("drop from Concrete")
}
}
trait Trait {}
impl Trait for Concrete {}
fn main() {
let a = Box::new(Concrete {}) as Box<dyn Trait>;
println!("-----");
}
-----
drop from Concrete
そもそも、Drop
トレイトは他のトレイトと違い、トレイトに対して実装できないし、割と想定通りの振る舞いでしょうか.
Drop was implemented on a trait, which is not allowed: only structs and enums can implement Drop.
引用元:https://doc.rust-lang.org/stable/error-index.html#E0120
Drop
と Copy
Drop
トレイトを実装している型に対して、#[derive(Copy)]
することはできません.実際にやってみると以下の通りコンパイルエラーになります.
#[derive(Copy)]
struct Sample;
impl Drop for Sample {
fn drop(&mut self) {}
}
error[E0184]: the trait `Copy` may not be implemented for this type; the type has a destructor
--> src/lib.rs:1:10
|
1 | #[derive(Copy)]
| ^^^^ Copy not allowed on types with destructors
|
= note: this error originates in a derive macro (in Nightly builds, run with -Z macro-backtrace for more info)
理由はCopy
トレイトのドキュメントに以下のようにあります.まあ当然で、RAIIしているのにmemcpyだけしても意味ないという理由のようです.
Generalizing the latter case, any type implementing Drop can't be Copy, because it's managing some resource besides its own
size_of::<T>
bytes.
引用元:https://doc.rust-lang.org/core/marker/trait.Copy.html#when-cant-my-type-be-copy
Drop::drop
が呼ばれないことを想定せよ
std::mem::forget
のドキュメントに以下のようにあるとおり、Drop::drop
が呼び出されることをsafe性の必要条件にしてはいけません.
forget
is not marked as unsafe, because Rust's safety guarantees do not include a guarantee that destructors will always run. For example, a program can create a reference cycle usingRc
, or callprocess::exit
to exit without running destructors. Thus, allowingmem::forget
from safe code does not fundamentally change Rust's safety guarantees.
引用元:https://doc.rust-lang.org/stable/std/mem/fn.forget.html#safety
例えば、Drop::drop
中で以下のようなことをさせている場合に、std::mem::forget
しても、それぞれファイルが開いたままになったり、ロックが確保されたままになったりするだけでunsafeには決してなりません.ですので、これらはsafeの設計として正しいことになります.
- 開いたファイルを閉じる
- 確保したMutex等のロックを開放する
ですが、std::mem::forget
した後に、コンストラクトするとメモリ破壊が起きるとかになってしまうと、それは何か設計が間違っているということです.
そもそもこの関数が何故safeなのかについては、以下の記事をご参照ください.簡単に言うとメモリリークはsafeということです.
std::mem::drop
好きなタイミングで値を破棄したい場合にはstd::mem::drop
を使います.
// 省略
fn main() {
let a = Sample::new("A");
{
let b = Sample::new("B");
std::mem::drop(a);
}
}
ctor A
ctor B
dtor A
dtor B
ところで、std::mem::drop
の実装は以下の通り空の関数です.
pub fn drop<T>(_x: T) {}
これを初めて見たとき、ある程度予想していたものの結構驚いたのですが、皆さんはどうでしょうか.仕組みは単純で、何もしない関数に値をムーブすれば、そのままライフライムが尽きるというだけです.
そのため、処理内容的には以下でもよいわけですが、デフォルト設定だと警告が出ます.実際意図が明確でないので避けるべきでしょう.
// 省略
fn main() {
let a = Sample::new("A");
{
let b = Sample::new("B");
a; // drop
}
}
warning: path statement drops value
--> src/main.rs:23:9
|
23 | a; // drop
| ^^^ help: use `drop` to clarify the intent: `drop(a);`
|
= note: `#[warn(path_statements)]` on by default
std::mem::forget
この関数を使うことでデストラクタが値の破棄時に呼び出されないようにできます.
// 構造体定義は同様なので省略
fn main() {
let a = Sample::new("A");
std::mem::forget(a);
}
ctor A
注意点としては、値を要求しているので少し使い勝手が悪いです(まあ文字通りforget
なので).その場合は後述するManuallyDrop
を直接使うとよいでしょう.
また、以下のようにある通り、あくまでデストラクタが呼び出されないだけで、自身のメモリリークを保証しているわけではないのでその点も注意しましょう.メモリリークさせたい場合はBox::leak
やBox::into_raw
が使えます.
However, it does not guarantee that pointers to this memory will remain valid.
引用元:https://doc.rust-lang.org/std/mem/fn.forget.html
メモリリークには使えないということなので、使いどころとしては、以下のような場合くらいでしょうか.
- 単に
Drop::drop
をスキップしたい - unsafe関数を使った結果、同じメモリを指している実体が2つできてしまったので、二重解放にならないように、一方を
std::mem::forget
する
この関数の実装も以下の通り非常に単純で、ManuallyDrop
に包んでいるだけです.ManuallyDrop
はその名の通りDrop
をマニュアルで扱うための構造体で、明示的に呼び出さない限りT
のデストラクタが呼ばれません.
pub const fn forget<T>(t: T) {
let _ = ManuallyDrop::new(t);
}
std::mem::ManuallyDrop
ManuallyDrop
を使うことでデストラクタの制御が柔軟に行えるようになります.Deref<Targe=T>
を実装しているので、T
型と同じように扱えますし、ゼロコストなのでFFIとも相性がいい気がします(今のところ使わずに済んでいるので自信はありませんが).
#![allow(dead_code)]
use std::mem::ManuallyDrop;
struct Parent {
a: ManuallyDrop<Child>,
b: ManuallyDrop<Child>,
c: ManuallyDrop<Child>,
}
impl Parent {
fn new() -> Self {
Self {
a: ManuallyDrop::new(Child::new("a")),
b: ManuallyDrop::new(Child::new("b")),
c: ManuallyDrop::new(Child::new("c")),
}
}
}
impl Drop for Parent {
fn drop(&mut self) {
unsafe { ManuallyDrop::drop(&mut self.b); }
println!("drop Parent");
unsafe { ManuallyDrop::drop(&mut self.a); }
}
}
struct Child {
name: String,
}
impl Child {
fn new(name: &str) -> Self {
Self {
name: name.to_string(),
}
}
}
impl Drop for Child {
fn drop(&mut self) {
println!("drop Child: {}", self.name);
}
}
fn main() {
Parent::new();
}
drop Child: b
drop Parent
drop Child: a
moveできるならManuallyDrop::into_inner
の返り値を捨てるのが安全で一番なのですが、参照なのでMannualyDrop::drop
を使うしかありません.
以下のような、二重解放するような危険なコードもコンパイルが通ってしまいますし、MannualyDrop::drop
した後にManuallyDrop::into_inner
とか色々危険ことができてしまいますが、上の例のようにDrop::drop
内で使うくらいなら比較的安全です.
unsafe {
ManuallyDrop::drop(&mut self.b);
ManuallyDrop::drop(&mut self.b);
}
また、デストラクタを呼び出さないという目的なら、何もしなくてよいので安全です.
その他
力尽きた
その他参考記事
- オブジェクトのDrop順序 - yohhoyの日記
-
Drop Checkerの規則をちゃんと理解する - 簡潔なQ
- 記事にここまでの内容は盛り込んでいないが興味があれば是非
- stack overflow: Why does Rust not allow the copy and drop traits on one type?
-
Rustの場合
std::mem::forget
等の抜け道はありますが、これは意図してやらないとならないので例外的 ↩︎ -
入ってない人は是非 https://rust-jp.herokuapp.com ↩︎
Discussion