🍬

[Rust] Drop/dropあれこれ

commits12 min read

この記事はRust 3 Advent Calendar 2020の7日目の記事です.

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だと以下のように書くことで同様の振る舞いになります.

Rust Playroundで実行

#![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の話もあるが)の内容しか書かれていません.

https://doc.rust-jp.rs/rust-by-example-ja/trait/drop.html

https://doc.rust-jp.rs/book-ja/ch15-03-drop.html

最近、個人的にFFIとかしている中で、もう少し詳しく知りたくなったので色々まとめてみましたという記事です.

入れ子の場合の呼び出し順

一番知りたかった内容ですが、Dropを定義した型が入れ子になった場合に呼び出し順はどうなるのでしょうか.
公式リファレンスに以下のような記述があります.

The destructor of a type T consists of:

  1. If T: Drop, calling <T as std::ops::Drop>::drop
  2. 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型のデストラクタは以下からなる

  1. T: Dropを満たしていれば、<T as std::ops::Drop>::dropを呼ぶ
  2. 全てのフィールドのデストラクタを再帰的に実行する
    • 構造体の場合、フィールドは宣言順にdropされる
    • enumの場合、フィールドは宣言順にdropされる
    • タプルの場合、フィールドは順番通りdropされる
    • 配列や所有しているスライスの要素は先頭からdropされる
    • クロージャがムーブキャプチャした変数のdrop順序は未指定
    • トレイトオブジェクトではunderlying type(後述)のデストラクタが実行される
    • その他の型はそれ以上の処理を行わない(つまり1のみということ)

ということで、実際にコードを使って振る舞いを見てみます.

Rust Playgroundで実行

#![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が順番通り処理されるとしたときに想定される通りの順番になっていることが確認できました.

  1. T: Dropを満たしていれば、<T as std::ops::Drop>::dropを呼ぶ
  2. 全てのフィールドのデストラクタを再帰的に実行する
    • 構造体の場合、フィールドは宣言順に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

DropCopy

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 using Rc, or call process::exit to exit without running destructors. Thus, allowing mem::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ということです.

https://qnighy.hatenablog.com/entry/2017/04/14/070000

std::mem::drop

https://doc.rust-lang.org/std/mem/fn.drop.html

好きなタイミングで値を破棄したい場合には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

https://doc.rust-lang.org/std/mem/fn.forget.html

この関数を使うことでデストラクタが値の破棄時に呼び出されないようにできます.

// 構造体定義は同様なので省略

fn main() {
    let a = Sample::new("A");
    std::mem::forget(a);
}
出力
ctor A

注意点としては、値を要求しているので少し使い勝手が悪いです(まあ文字通りforgetなので).その場合は後述するManuallyDropを直接使うとよいでしょう.
また、以下のようにある通り、あくまでデストラクタが呼び出されないだけで、自身のメモリリークを保証しているわけではないのでその点も注意しましょう.メモリリークさせたい場合はBox::leakBox::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

https://doc.rust-lang.org/beta/std/mem/struct.ManuallyDrop.html

ManuallyDropを使うことでデストラクタの制御が柔軟に行えるようになります.Deref<Targe=T>を実装しているので、T型と同じように扱えますし、ゼロコストなのでFFIとも相性がいい気がします(今のところ使わずに済んでいるので自信はありませんが).

Rust Playgroundで実行

#![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);
}

また、デストラクタを呼び出さないという目的なら、何もしなくてよいので安全です.

その他

力尽きた

その他参考記事

脚注
  1. Rustの場合std::mem::forget等の抜け道はありますが、これは意図してやらないとならないので例外的 ↩︎

  2. 入ってない人は是非 https://rust-jp.herokuapp.com ↩︎

GitHubで編集を提案

Discussion

ログインするとコメントできます