Rustトレイトの既定実装と否定実装

概要: SendやSyncなど一部のトレイトで採用されている機能である、既定実装と否定実装の挙動を調べた。

既定実装と否定実装について

既定実装(デフォルト実装, 自動実装, オート実装, default impl, auto impl)と否定実装(negative impl)はRust RFC 0019: Opt-in Builtin Traits (OIBITs)にて、unsafeトレイトとともに規定されている機能である。

OIBITsは、その名前に反して、「言語に組み込みとは限らないマーカートレイトについて、オプトアウト方式での実装を可能にする」という、非常に天邪鬼な機能である。

既定実装と否定実装は組み合わせて使われる。実際の標準ライブラリの例を見るとわかりやすい。

#![feature(optin_builtin_traits)]

// unsafeトレイト
unsafe trait Send {}
// unsafeな既定実装
unsafe impl Send for .. {}
// 否定実装
impl<T:?Sized> !Send for Arc<T> {}

標準ライブラリのOIBITs

1.16.0現在、標準ライブラリで既定実装と否定実装が使われているのは以下の4つである。

既定実装

既定実装は以下のような構文を持つ。

unsafe impl Trait for .. {}
impl Trait for .. {}

ただし、以下の制約がある。

  • この実装は生存期間・型パラメーターをとることができない。
  • 実装対象のトレイトは生存期間・型パラメーターをとることができない。
  • 既定実装は中身を持つことができない。したがってマーカートレイトに対してしか使うことができない。 (このトレイトがwhere節をもつことは可能である。)

既定実装の適用規則

既定実装をもつトレイトは、トレイト束縛の解決時にその内容が参照される。以下の条件で、既定実装が適用される。 (rustc::traits::select 2104行目)

  • 否定実装を含め、他の実装が見つかっていない。 (rustc::traits::select 1122行目)
    • where制約を満たしていない実装でも、型のパターンが一致していれば、この時点では「見つかっている」と見なす。
  • トレイト自身にwhere制約がある場合、それを満たしている。
  • この型を構成する型も、同じトレイトを実装している。ただし、「構成する型」は以下のように定義される。
    • PhantomData<T> は、 T から構成される。
    • それ以外の構造体や列挙体は、メンバ全てから構成される。 (クロージャも似ているが #27086 が関係してくる)
    • 配列もポインタも参照も、その元の型から構成される。

これらが where に追加されたのと同様に振る舞うと考えればよい。

既定実装の上書き

上に書いてあるように、既定実装の生成するwhere制約が所望のものではなかったときは、自分で定義した実装で上書きできる。

例えば、

#![feature(optin_builtin_traits)]

trait Foo {}

impl Foo for .. {}

struct B<X>(X);

の場合、 B<X> には以下の実装が生成されたような扱いになる。

impl<X: Foo> Foo for B<X> {}

例えば、以下のように書くと、 B に対しては既定実装は適用されなくなり、かわりに明記された実装が使われるようになる。 XはFooでなくてもよいが、Barである必要があるようになる。

trait Bar {}
impl<X: Bar> Foo for B<X> {}

以下の場合、 B<u32> については上記の実装を適用し、それ以外については B<X> であっても既定実装を適用することになる。

impl Foo for B<u32> {}

否定実装

否定実装は以下のような構文を持つ。

impl<'a, X> !Trait for Type<'a, X> {}

ただし、以下の制約がある。

  • 当たり前だが、inherent implとして (impl !Type {}のように) 使うことはできない。
  • 実装対象のトレイトは既定実装を持つ必要がある。したがって否定実装は中身を持たないし、マーカートレイトに対してしか使うことができない。 (このトレイトがwhere節をもつことは可能である。)

否定実装は、この実装が採用されたときにエラーとなるという点以外は、普通の実装と同様である。これは既定実装の上書きをするときに、where条件を変えるのではなく実装を丸ごと禁止するのに使う。

まとめ

既定実装と否定実装は Send/Sync/UnwindSafe/RefUnwindSafe のように、基本的には特定の継承ルールに基づいてマーカートレイトを実装させたいが、特定の型に対しては異なるルールを適用したいときに用いる。

Rustの組み込みマクロ

Rustのマクロの多くは macro_rules! で定義されるが、トークン列をトークン列に変換するものなら何でもマクロとして実装されうる。

Rustの標準ライブラリのマクロの多くは core::macros にて定義されている。

以下のマクロは通常の macro_rules! により定義される。

  • panic!
  • assert!
  • assert_eq!
  • assert_ne!
  • debug_assert!
  • debug_assert_eq!
  • debug_assert_ne!
  • try!
  • write!
  • writeln!
  • unreachable!
  • unimplemented!

以下のマクロは core::macros::builtin に宣言だけ存在するが、実装はコンパイラ組み込みである。

  • format_args!
  • env!
  • option_env!
  • concat_idents!
  • concat!
  • line!
  • column!
  • file!
  • stringify!
  • include_str!
  • include_bytes!
  • module_path!
  • cfg!
  • include!

これらはコンパイラ組み込みであり、 syntax_ext::register_builtins で登録されている。

これはマクロに限らない一般の構文拡張を管理するデータベース syntax::ext::base で管理されている。このデータベースには以下のようなエントリがある(抜粋):

  • MultiModifier: Pythonの属性のように、itemを受け取り別のitemに変換する構文拡張。
  • ProcMacro: トークン列をトークン列に変換する、構文拡張の一般的なインターフェース。
  • AttrProcMacro: ProcMacro に似ているが、属性込みで変換できるインターフェース。
  • NormalTT: ProcMacro に似ているが、よりマクロ呼び出しに特化したインターフェースになっている。トークン列ではなく、構文要素を返す。
  • IdentTT: 識別子を識別子に変換する。

マクロ構文では ProcMacro, NormalTT, IdentTT のみ取り扱われる。他の構文拡張はattrやderiveで使われる。

macro_rules! で定義されるマクロは syntax::ext::tt::macro_rulesNormalTT に変換される。また組み込みマクロも NormalTT である。なお、 macro_rules! 自身はかなり特殊な扱いで、構文解析器が場合分けを行う。

組み込みマクロは、上で説明したように、トークン列を構文要素に変換する関数がコンパイラ内に実装されているという以上のことはない。 include_str!stringify! のようなCプリプロセッサのような処理は syntax::ext::source_util で実装されている。それ以外の組み込みマクロの実装は src/libsyntax_ext内の各ソースファイルが提供している。

まとめ

Rustにおけるマクロは、トークン列を構文要素に変換するメタ関数に他ならない。多くは macro_rules! により構文解析時に定義されるが、これで定義できるのはごく簡単なマクロだけである。より複雑だったり、処理系組み込みの機能を提供するマクロは、コンパイラに組み込みであったり、 別のcrateでプロシージャマクロとして定義されていたりする。

Rustのマングリング(名前修飾)

Rustの生成したネイティブコードを見ると、 _ZN6thread5sleep20h87eee61de4645181cAbE のようなシンボル名が見える。この例は std::thread::sleep に対応している。このようにRustの(単相化された)アイテム(関数や変数など)の名前をリンカが認識できる文字列にエンコードする処理はマングリングと呼ばれている。

Rustのマングリングは一見してわかるようにC++互換になっている。実際に上の関数をデマングルすると

$ c++filt _ZN6thread5sleep20h87eee61de4645181cAbE
thread::sleep::h87eee61de4645181cAb

と、それらしき名前が出てくる。

しかし、これは表層上C++互換になっているというだけで、Rustのマングリングの本質はこのハッシュ部分 h87eee61de4645181cAb に依拠している。

以下、Rust 1.15.1の rustc_trans::back::symbol_names に基づいて、Rustのマングリングについて説明する。これはコンパイラのバージョンアップにより情報が古くなる可能性がある。

DefPathを計算する

まず、Rustのアイテムの単相化前の部分を一定の形にエンコードする必要があるが、これには rustc::hir::map::definitions::DefPath が使われる。

DefPathは簡単に言うと、 std::result::Result::map のように、単相化前のアイテムの位置を、crateのルートからの相対パスとして保持したものである。ただしそれだけでは関数内関数など、同じ名前で複数のアイテムを定義できる場合がある。そのため、 foo[1]::bar[0], foo[1]::bar[1] といった感じで番号 (disambiguator) を与えて区別する。disambiguatorは0から順に振られるため、通常は 0 である。

DefPathの定義を抜粋すると以下のようになる。

pub struct DefPath {
    pub data: Vec<DisambiguatedDefPathData>,
    pub krate: CrateNum,
}

pub struct DisambiguatedDefPathData {
    pub data: DefPathData,
    pub disambiguator: u32
}

pub enum DefPathData {
    CrateRoot,
    InlinedRoot(Box<InlinedRootPath>),
    Misc,
    Impl,
    TypeNs(InternedString),
    ValueNs(InternedString),
    Module(InternedString),
    MacroDef(InternedString),
    ClosureExpr,
    TypeParam(InternedString),
    LifetimeDef(InternedString),
    EnumVariant(InternedString),
    Field(InternedString),
    StructCtor,
    Initializer,
    Binding(InternedString),
    ImplTrait
}

DefPathDisambiguatedDefPathDataHash をderiveしているが、今回のハッシュ化に DefPath:Hash の実装は使わない。というのも krate: CrateNum は内部的なu32のidで表現されており、これをそのまま使うとコンパイラの実行ごとにハッシュが安定しないからである。

安定ハッシュを用意する

Rustにはオブジェクトをハッシュ化するための Hash というトレイトが用意されているが、通常のハッシュ実装ではアーキテクチャ依存性が発生するためにクロスコンパイルで問題が生じる可能性がある。というのも、 Hash が利用する Hasher は以下のようなインターフェースを持ち、……

pub trait Hasher {
    fn finish(&self) -> u64;
    fn write(&mut self, bytes: &[u8]);

    fn write_u8(&mut self, i: u8) { ... }
    fn write_u16(&mut self, i: u16) { ... }
    fn write_u32(&mut self, i: u32) { ... }
    fn write_u64(&mut self, i: u64) { ... }
    fn write_usize(&mut self, i: usize) { ... }
    fn write_i8(&mut self, i: i8) { ... }
    fn write_i16(&mut self, i: i16) { ... }
    fn write_i32(&mut self, i: i32) { ... }
    fn write_i64(&mut self, i: i64) { ... }
    fn write_isize(&mut self, i: isize) { ... }
}

……しかもこれらのデフォルト実装は、「現在のアーキテクチャエンディアンでバイト列に変換して write に突っ込む」となっているからである。

Rustのコンパイラはこれに対処するため、 isizeusize を使っても常に同じようにハッシュされるHasherである rustc_data_structures::stable_hasher::StableHasher を用意している。これはblake2bのラッパーだが以下のように動作する:

  • i8u8 は1バイトとして書き込む。
  • 残りの全ての整数型は、リトルエンディアンの128bit整数に変換して書き込む。

型パラメーターとあわせてハッシュ化する

64bitの安定ハッシャーを用意して、以下の順に突っ込む。

  • DefPath (ただしcrateが安定するように工夫してある)
  • アイテム自身の型 (単相化する前)
  • 多相ならば、その型代入

ハッシュを64bitで取り出し、16進表記して接頭辞 h をつける。 (例: h87eee61de4645181cAb)

飾り付けをする

再び、アイテムの絶対パスを計算して、先ほど生成したハッシュと連結する。 (例: thread/sleep/h87eee61de4645181cAb)

サニタイズする

変な文字が入っているかもしれないので、簡単な文字だけからなるように変換する。

  • @$SP$
  • *$BP$
  • &$RF$
  • <$LT$
  • >$GT$
  • ($LP$
  • )$RP$
  • ,$C$
  • :.
  • -.
  • aからz, AからZ, 0から9, _, ., $ → そのまま
  • それ以外のUnicode文字 → $u2764$ のようにコードポイントを16進数で

C++互換でマングリングする

_ZN + (文字数を10進数で + 文字列) ×任意個 + E

できあがり

以上の処理は rustc_trans::trans_item::TransItem::compute_symbol_name で使われている。

まとめ

Rustはアイテムの位置と型引数をできるだけ安定性の高い方法でハッシュ化し、それをシンボル名として使っている。読みやすさのために、C++互換の情報も付与されている。

ハッシュは h + 64bit で17文字のはずなのに実例をみると20文字になっている原因がまだ把握できていない。

Rustのvtableの内部構造

trait objectは、型情報を忘れるかわりにvtableへの参照を持ち回すことで動的ディスパッチを実現している。

vtableを生成するコードはrustc_trans::meth 112行目にある。これによると、vtableの構造は以下のとおり。

  • 0番目: Drop Glue をあらわす関数ポインタ (デストラクタ)
  • 1番目: 元の型のバイト数
  • 2番目: 元の型のアラインメント
  • 3番目以降: メソッドへのポインタ (宣言順)
    • Self: Sized 制約のついたメソッドに対応するスロットには0が代入される。

trait objectはSizedではないので、値をコピー渡しすることはなく、fat pointerで渡す。fat pointerの0番目は、もとのデータと同じ先頭番地を指すポインタで、fat pointerの1番目が、vtableの先頭番地を指すポインタである。

実際にvtableを手動で取り出して実験してみたものが以下のコードである。

use std::mem::transmute;

trait Foo {
    fn fn1(&self, u32);
    fn fn2(&self, u32) where Self: Sized;
    fn fn3(&self, u32);
}

#[derive(Debug)]
struct S1(u64, u64, u64);

impl Foo for S1 {
    fn fn1(&self, x: u32) {
        println!("fn1({:?}, {})", self, x);
    }
    fn fn2(&self, x: u32) where Self: Sized {
        println!("fn2({:?}, {})", self, x);
    }
    fn fn3(&self, x: u32) {
        println!("fn3({:?}, {})", self, x);
    }
}

impl std::ops::Drop for S1 {
    fn drop(&mut self) {
        println!("drop({:?})", self);
    }
}

struct FooVtable {
    drop_glue: fn(&mut S1),
    size: usize,
    align: usize,
    fn1_ptr: fn(&S1, u32),
    fn2_ptr: fn(&S1, u32),
    fn3_ptr: fn(&S1, u32),
}

fn recover_S1(foo: &mut Foo) -> (&mut S1, &'static FooVtable) {
    unsafe { transmute(foo) }
}

fn main() {
    let mut x = S1(3, 4, 5);
    let foo : &mut Foo = &mut x;
    let (xx, vtbl) = recover_S1(foo);
    println!("vtbl.drop_glue = {:x}", vtbl.drop_glue as usize);
    println!("vtbl.size = {:x}", vtbl.size);
    println!("vtbl.align = {:x}", vtbl.align);
    println!("vtbl.fn1_ptr = {:x}", vtbl.fn1_ptr as usize);
    println!("vtbl.fn2_ptr = {:x}", vtbl.fn2_ptr as usize);
    println!("vtbl.fn3_ptr = {:x}", vtbl.fn3_ptr as usize);
    println!("S1::fn1 = {:x}", S1::fn1 as usize);
    println!("S1::fn2 = {:x}", S1::fn2 as usize);
    println!("S1::fn3 = {:x}", S1::fn3 as usize);
    (vtbl.drop_glue)(xx);
    (vtbl.fn1_ptr)(xx, 88);
    (vtbl.fn3_ptr)(xx, 188);
}
vtbl.drop_glue = 55c14c8def80
vtbl.size = 18
vtbl.align = 8
vtbl.fn1_ptr = 55c14c8df0c0
vtbl.fn2_ptr = 0
vtbl.fn3_ptr = 55c14c8df340
S1::fn1 = 55c14c8df0c0
S1::fn2 = 55c14c8df200
S1::fn3 = 55c14c8df340
drop(S1(3, 4, 5))
fn1(S1(3, 4, 5), 88)
fn3(S1(3, 4, 5), 188)
drop(S1(3, 4, 5))

drop glueは別として、メソッドテーブルに入っている関数ポインタは元のメソッドの使い回しであることがわかる。

むろん、このような荒技を実験以外に使うのは好ましくない。

RustでPhantomDataにT以外のものを入れるのはなぜか

PhantomDataにT以外のものを入れるのは、主に3つの理由がある。

  • 生存期間変数に言及するため。
  • 変性を制御するため。
  • 所有関係を制御するため。

生存期間変数に言及するため

型だけではなく、生存期間変数も、迷子になっては困る。こういうときは参照を使って PhantomData<&'a T> のように書くことが多い。

変性を制御するため

PhantomData<T>T に対して共変である。つまり、 T より広い型 S があったとき(これは通常、生存期間変数を入れ替えることで発生する)、 PhantomData<S>PhantomData<T> より広いとみなされる。

現在のRustは、内部的には非変・共変・反変・双変の4つの変性を持つ。反変が今後も残るかはわからないが、非変と共変のどちらをとるかは場合による。

非変にするときはよく PhantomData<fn(T) -> T> と書く。

所有関係を制御するため

Drop Checkの関係で、「ある型がある型を(直接的に/間接的に)所有しているか」というのがborrow checkerの挙動に影響を与えてしまう。

この所有判定において、 PhantomData<T>T を所有しているとみなされる。所有していないとみなしたい場合は PhantomData<*const T>PhantomData<&'a T> を使うことが多い。

まとめ

PhantomDataは単に型や生存期間変数を迷子にしないという以上に、変性と所有関係という2つの隠し属性の制御にも用いられる。これらのためのイディオムがいくつか知られている。

Rustで型の多相再帰はできない

OCamlHaskellに比べると、Rustは多相再帰ができない場合がほとんどである。以下にその詳細を説明する。

多相再帰

異なる型引数による再帰呼び出しを多相再帰 (polymorphic recursion) という。多相再帰はPurely Functinoal Data Structuresで紹介されているようなデータ構造でよく出てくる。例えば、完全二分木はOCamlHaskellではそれぞれ以下のように書ける。

type 'a sep = Nil | Cons of ('a * 'a) sep
data Sep a = Nil | Cons (Sep (a, a))

これがlistの定義と異なることがわかるだろうか。listでは 'a list の定義に 'a list という形の型のみを用いる。ここでは 'a sep の定義に ('a * 'a) sep を用いている。これが多相再帰である。

Rustにおける型の多相再帰

同じものをRustで書こうとすると次のようになる。

enum Sep<T> {
    Nil,
    Cons(T, Box<Sep<(T, T)>>),
}

fn main() {
    let x = Sep::Nil::<u32>;
}

しかしこれはコンパイルが通らない。例えばコンパイラが無限ループに陥りkillされる。

再帰型だけではなく再帰関数についても同じことが起こる。以下のコードはやはり再帰制限に引っかかる。

fn f<T>(n: u32, x: T) {
    if n > 0 {
        f::<Option<T>>(n-1, Some(x));
    }
}

fn main() {
    f(1, true);
}

多相再帰ができない理由

OCamlHaskellはパラメーター多相性を実行時に解決している(ジェネリックス)のに対し、Rustはパラメーター多相性をコンパイル時に解決する(テンプレート)。

上の例では、 Sep<u32> ができたことにより、Rustは Sep<u32> のための専用のコードを生成しようとする。ところが、この型は内部に Sep<(u32, u32)> を持ちうるため、これに関係するコードの生成が必要になる。さらにその内部では Sep<((u32, u32), (u32, u32))> が必要になる。この繰り返しになってしまうためコンパイルができない。

OCamlHaskellなら、 int sep 専用ではなく 'a sep 用の汎用のコードを生成して終わりなので、特に困ることはない。

幽霊型も使用不可能

OCamlHaskellの内部表現を真似て、 TBox にくるんでもコンパイルエラーはなくならない。

enum Sep<T> {
    Nil,
    Cons(Box<T>, Box<Sep<(T, T)>>),
}

fn main() {
    let x = Sep::Nil::<u32>;
}

完全に型情報を捨てて Any にしてしまえば、もちろんコンパイルは通る。

use std::any::Any;

enum Sep {
    Nil,
    Cons(Box<Any>, Box<Sep>),
}

fn main() {
    let x = Sep::Nil::<u32>;
}

これだと心許ないので、幽霊型をつけてみると、コンパイルは失敗する。

struct BoxAny<T> {
    val: Box<std::any::Any>,
    _phantom: std::marker::PhantomData<T>,
}

enum Sep<T> {
    Nil,
    Cons(BoxAny<T>, BoxAny<Sep<(T, T)>>),
}

fn main() {
    let x = Sep::Nil::<u32>;
}

結局、幽霊型の場合も、幽霊型引数の違いごとに別々にコード生成をしなくてはならないため、無限コード生成を回避することはできない。

多相再帰自体が禁止されているわけではない

実際は、多相再帰自体は禁止されているわけではない。無限にたくさんの型が発生しない限りは、多相再帰があっても問題ない。

人工的な例だが、次のようなプログラムは動作する。

use std::marker::PhantomData;

struct Z;
struct S<X>(PhantomData<X>);

trait Nat {
    type Pred : Nat;
    fn to_u32() -> u32;
}

impl Nat for Z {
    type Pred = Z;
    fn to_u32() -> u32 { 0 }
}
impl<X:Nat> Nat for S<X> {
    type Pred = X;
    fn to_u32() -> u32 { X::to_u32() + 1 }
}

fn f<X:Nat>() -> u32 {
    if(X::to_u32() == 0) {
        1
    } else {
        f::<X::Pred>() * 2
    }
}

fn main() {
    println!("{}", f::<S<S<S<Z>>>>());
}

ここで f<S<S<S<Z>>>> を生成しようとすると f<S<S<Z>>> が必要になる。これにはさらに f<S<Z>> が必要になる。さらに f<Z> が必要になる。しかし Z::PredZ だから、これ以上必要なものはない。

生存期間に関する多相再帰は可能

ここまでで言及したのは、型に関する多相再帰である。生存期間は型とは異なり、コンパイル時には型消去されるだけなので、実行時に無限に多くの生存期間が生じうることに全く問題はない。

むしろ、普通にRustコードを書いていると、生存期間に関する多相再帰が発生していると考えてよいだろう。例えば、

enum List<X> {
    Nil,
    Cons(X, Box<List<X>>),
}

fn length<'a, X>(list: &'a List<X>) -> u32 {
    match list {
        &List::Nil => 0,
        &List::Cons(_, ref tail) => length(tail) + 1,
    }
}

fn main() {}

において、 listtail の生存期間は明らかに異なる。

まとめ

  • Rustでは、多相再帰そのものが禁止されているわけではない。
  • 生存期間に関する多相再帰は全く問題がないどころか、頻繁に使われている。
  • 型に関する多相再帰の多くは、無限に多くの型を生成してしまうために、失敗する。
  • 幽霊型でも同じ問題が起きる。

RustのDropの実装に対する制約とDrop Checkの規則

DropとDrop CheckについてはThe Book: DropNomicon: Drop Checkを読んでもらうとして、この辺りの規則はコンパイラtypeck::check::dropckのコメントに解説がある。

Dropの非伝搬性

ところで、筆者が個人的に勘違いしていた点として、「 T: Drop である ⇔ T の破棄時に何かが実行される」という思い込みがあった。実際には例えば、

struct A<T>(T);

let x = A(Box::new(true));

とやると、 A<Box<bool>>: Drop ではないが、 x の破棄時には Box のdropが実行される。

Dropdrop を実装するためのトレイトにすぎず、これらの性質を親に伝搬させるマーカーの役割は果たしていないということになる。

Dropの実装に対する制約

typeck::check::dropck::check_drop_implドキュメンテーションによると、Dropの実装はある特定の要件を満たす必要がある。簡単に言うと、他のトレイトとは異なり、 Drop は特殊化が禁止されている。型引数によって Drop が実装されたりされなかったりする、ということが起きないようになっている。

駄目な実装の例を以下に挙げる。

use std::ops::Drop;

struct A<X, Y>(X, Y);

// impl<X> Drop for A<X, u32> { fn drop(&mut self) {}} // Bad
// impl<X> Drop for A<X, X> { fn drop(&mut self) {}} // Bad
impl<Y, X> Drop for A<X, Y> { fn drop(&mut self) {}} // Good

struct B<X: ?Sized>(Box<X>);

// impl<X: ?Sized + Clone> Drop for B<X> { fn drop(&mut self) {}} // Bad
// impl<X> Drop for B<X> { fn drop(&mut self) {}} // Bad
impl<X: ?Sized> Drop for B<X> { fn drop(&mut self) {}} // Good

Drop Checkerの規則

Drop Checkerの規則はSound Generic Dropと呼ばれるが、これには2つのバージョンがある。

RFC0769は、それ以前に存在していた #[unsafe_destructor] というescape hatchを不要にする目的で提案された。これはRustの型にparametricityという仮定(簡単にいうと、型引数による場合分けは発生しないという仮定)をおくことで、より積極的なDrop安全性判定を行うというものであった。

RFC1238は主に、Rust RFC 1210: Impl specialization のように、parametricityの仮定を崩すような拡張を入れるために、RFC0769の変更点の一部を差し戻す方向に再変更するものである。これによりDrop安全性判定はより保守的になったため、新たなescape hatchとして #[unsafe_destructor_blind_to_params] が追加された。

現在のRFC1238にもとづく規則では、Drop Checkerは以下の規則を検証する。

v を (一時的な値か名前のついた値かに関係なく) なんらかの値とし、 'a を生存期間 (スコープ) とする。 もし、 v がある型 D のデータを所有していて、しかも

  1. D が生存期間引数か型引数でパラメーター化された Drop 実装 (であって、もちろん unsafe_destructor_blind_to_params でチェックの迂回が指示されていないもの) を持っていて、かつ
  2. D の構造から型 &'a _ の参照へ到達できる

とき、 'av よりも真に長く生存しなければならない。

(check_safety_of_destructor_if_necessary のdoc-commentから翻訳した)

それ以前のオリジナルのSound Generic Dropでは、前提が1つ多いためルールが緩い。上と対比する形で書くと次のようになる。

v を (一時的な値か名前のついた値かに関係なく) なんらかの値とし、 'a を生存期間 (スコープ) とする。 もし、 v がある型 D のデータを所有していて、しかも

  1. D が生存期間引数か型引数でパラメーター化された Drop 実装を持っていて、かつ
  2. D の構造から型 &'a _ の参照へ到達できて、さらに
  3. 次のどちらかが満たされる:
    • (A.) DDrop 実装は 'a を直接具体化している (つまり D<'a>) か、
    • (B.) DDrop 実装は 1つ以上のメソッドを持つトレイト T に制約された型引数を1つ以上持っている

とき、 'av よりも真に長く生存しなければならない。

(1つ前の翻訳に追記する形で、Rust RFC 0769から翻訳した。)

Drop Checkerの型トラバース

Drop Checkerは型のメンバに対して再帰的に所有関係を検査する。この規則は以下のようになっている。

  • Box, 配列、スライスはその要素を所有する。 (要素数0の配列でも!)
  • PhantomData<T>は例外的に、 T を所有しているものとみなす。
  • BoxPhantomData 以外の構造体と列挙体は、とりえる全てのメンバを所有する。
  • ポインタ、参照は参照先を所有しない。
  • 型パラメーターは、何も所有していない。(これは関数の型パラメーターから由来するもので、上で問題になっているparametricityとは別の箇所である。)

まとめ

Dropの挙動は複雑なうえに仕様の変動がある。また、Dropチェッカーは構造体の隠されたフィールドにも影響を受けるので、leaky abstractionの心配がある。