22

のドキュメントはmem::uninitialized、その関数を使用することが危険/安全ではない理由を指摘してdropいます。初期化されていないメモリでの呼び出しは、未定義の動作です。

したがって、このコードは未定義である必要があります。

let a: TypeWithDrop = unsafe { mem::uninitialized() };
panic!("=== Testing ==="); // Destructor of `a` will be run (U.B)

ただし、安全なRustで動作し、未定義の動作に悩まされていないように見える次のコードを書きました。

#![feature(conservative_impl_trait)]

trait T {
    fn disp(&mut self);
}

struct A;
impl T for A {
    fn disp(&mut self) { println!("=== A ==="); }
}
impl Drop for A {
    fn drop(&mut self) { println!("Dropping A"); }
}

struct B;
impl T for B {
    fn disp(&mut self) { println!("=== B ==="); }
}
impl Drop for B {
    fn drop(&mut self) { println!("Dropping B"); }
}

fn foo() -> impl T { return A; }
fn bar() -> impl T { return B; }

fn main() {
    let mut a;
    let mut b;

    let i = 10;
    let t: &mut T = if i % 2 == 0 {
        a = foo();
        &mut a
    } else {
        b = bar();
        &mut b
    };

    t.disp();
    panic!("=== Test ===");
}

他のデストラクタを無視しながら、常に正しいデストラクタを実行しているようです。aまたはb(のa.disp()代わりに)を使用しようとするt.disp()と、初期化されていないメモリを使用している可能性があると正しくエラーが発生します。私が驚いたのはpanic、の値に関係なく、常に正しいデストラクタを実行する (期待される文字列を出力する) ことiです。

これはどのように起こりますか?ランタイムが実行するデストラクタを決定できる場合、実装された型に対して強制的に初期化する必要があるメモリに関する部分を、上記のリンクのDropドキュメントから削除する必要がありますか?mem::uninitialized()

4

3 に答える 3

21

ドロップ フラグの使用。

Rust (バージョン 1.12 まで) は、型が実装されているすべての値にブール値フラグを格納しますDrop(したがって、その型のサイズが 1 バイト増加します)。そのフラグは、デストラクタを実行するかどうかを決定します。したがって、変数b = bar()のフラグを設定すると、のデストラクタのみが実行されます。逆に.bba

Rust バージョン 1.13 以降 (ベータ コンパイラの執筆時点) では、フラグは型に格納されず、すべての変数または一時的なスタックに格納されることに注意してください。これは、Rust コンパイラに MIR が登場したことで可能になりました。MIR は、Rust コードからマシン コードへの変換を大幅に簡素化し、この機能でドロップ フラグをスタックに移動できるようにしました。最適化は通常、コンパイル時にどのオブジェクトが削除されるかを判断できる場合、そのフラグを削除します。

タイプのサイズを調べることで、バージョン 1.12 までの Rust コンパイラでこのフラグを「観察」できます。

struct A;

struct B;

impl Drop for B {
    fn drop(&mut self) {}
}

fn main() {
    println!("{}", std::mem::size_of::<A>());
    println!("{}", std::mem::size_of::<B>());
}

0とは1それぞれスタック フラグの前に出力し、00はスタック フラグを付けて出力します。

ただし、コンパイラは変数への割り当てを認識し、ドロップ フラグを設定するため、使用mem::uninitializedは依然として安全ではありません。aしたがって、デストラクタは初期化されていないメモリで呼び出されます。Dropあなたの例では、 implはあなたのタイプのメモリにアクセスしないことに注意してください(ドロップフラグを除くが、それはあなたには見えません)。そのため、初期化されていないメモリにアクセスしていません(タイプがゼロサイズの構造体であるため、とにかくサイズがゼロバイトです)。私の知る限り、これはunsafe { std::mem::uninitialized() }コードが実際に安全であることを意味します。

于 2016-09-28T14:54:46.467 に答える
18

ここには 2 つの質問が隠されています。

  1. コンパイラはどの変数が初期化されているかどうかをどのように追跡しますか?
  2. 初期化するmem::uninitialized()と未定義の動作が発生するのはなぜですか?

順番に取り組んでいきましょう。


コンパイラはどの変数が初期化されているかどうかをどのように追跡しますか?

コンパイラは、いわゆる「ドロップ フラグ」を挿入します。スコープの最後で実行する必要がある変数ごとDropに、この変数を破棄する必要があるかどうかを示すブール フラグがスタックに挿入されます。

フラグは「いいえ」から始まり、変数が初期化されると「はい」に移動し、変数が移動されると「いいえ」に戻ります。

最後に、この変数をドロップするときが来ると、フラグがチェックされ、必要に応じてドロップされます。

これは、コンパイラのフロー分析が、初期化されていない可能性のある変数について不平を言うかどうかとは関係ありません。フロー分析が満たされた場合にのみ、コードが生成されます。


初期化するmem::uninitialized()と未定義の動作が発生するのはなぜですか?

you を使用するときmem::uninitialized()は、コンパイラに対して次のことを約束します。心配しないでください。間違いなく this を初期化しています。

したがって、コンパイラに関する限り、変数は完全に初期化され、ドロップ フラグは "yes" に設定されます (変数から移動するまで)。

これは、Dropが呼び出されることを意味します。

初期化されていないオブジェクトを使用することは未定義の動作であり、Dropユーザーに代わって初期化されていないオブジェクトを呼び出すコンパイラは、「それを使用している」と見なされます。


ボーナス:

私のテストでは、奇妙なことは何も起こりませんでした!

未定義の動作とは、何でも起こり得ることを意味することに注意してください。残念ながら、「うまくいくように見える」(または「確率にもかかわらず意図したとおりに機能する」)ことも含まれます。

特に、(印刷のみで) オブジェクトのメモリにアクセスしない場合、Drop::dropすべてが正常に機能する可能性が非常に高くなります。ただし、アクセスすると、奇妙な整数、ワイルドを指すポインターなどが表示される場合があります...

そして、オプティマイザーが賢い場合、アクセスしなくても、おかしなことをするかもしれません! 私たちは LLVM を使用しているので、Chris Lattner (LLVM の父) による、すべての C プログラマーが未定義の動作について知っておくべきことを読むことをお勧めします。

于 2016-09-28T15:16:26.900 に答える
3

まず、ドロップ フラグがあります。これは、どの変数が初期化されたかを追跡するための実行時情報です。変数が割り当てられていない場合、 はdrop()実行されません。

安定版では、ドロップ フラグは現在型自体に格納されています。初期化されていないメモリを書き込むと、drop()呼び出されるかどうかに関して未定義の動作が発生する可能性があります。これは、ナイトリーでドロップ フラグがタイプ自体から移動されるため、すぐに古い情報になります。

nightly Rust では、初期化されていないメモリを変数に割り当てると、それdrop()が実行されると想定しても問題ありません。ただし、 の有用な実装はdrop()値に対して動作します。型が適切に初期化されているか、Dropトレイト実装内にないかを検出する方法はありません。型の実装によっては、無効なポインターまたはその他のランダムなものを解放しようとする可能性がありますDrop。とにかく、初期化されていないメモリを型に割り当てることDropはお勧めできません。

于 2016-09-28T15:07:11.223 に答える