16

この回答で述べたように、デストラクタを 2 回目に呼び出すだけでは、未定義の動作 12.4/14(3.8) になります。

例えば:

class Class {
public:
    ~Class() {}
};
// somewhere in code:
{
    Class* object = new Class();
    object->~Class();
    delete object; // UB because at this point the destructor call is attempted again
}

この例では、デストラクタを複数回呼び出すことができるようにクラスが設計されています - 二重削除のようなことは起こりません。が呼び出された時点で、メモリはまだ割り当てられています。delete最初のデストラクタ呼び出しでは、::operator delete()メモリを解放するために が呼び出されません。

たとえば、Visual C++ 9 では、上記のコードは機能しているように見えます。UB の C++ 定義でさえ、UB として認定されたものの動作を直接禁止するものではありません。そのため、上記のコードが実装やプラットフォームの仕様を壊すには、いくつかの仕様が必要です。

上記のコードが正確に壊れる理由と、どのような条件でしょうか?

4

16 に答える 16

14

あなたの質問は、標準の背後にある理論的根拠を目指していると思います。逆に考えてみてください:

  1. デストラクタを 2 回呼び出す動作を定義すると、多くの作業が発生する可能性があります。
  2. あなたの例は、いくつかの些細なケースでは、デストラクタを2回呼び出しても問題にならないことを示しているだけです。それは本当ですが、あまり面白くありません。
  3. デストラクタを2回呼び出すことが何らかの形で良い考えである/コードをより簡単にする/言語をより強力にする/セマンティクスをクリーンアップする/またはその他の場合、説得力のあるユースケースを提供しませんでした(そして私はあなたができるとは思えません)。

では、これが未定義の動作を引き起こしてはならないのはなぜでしょうか?

于 2010-05-05T08:44:36.463 に答える
8

デストラクタを呼び出した後、オブジェクトは存在しなくなります。

したがって、もう一度呼び出すと、存在しないオブジェクトのメソッドを呼び出すことになります。

なぜこれが定義された動作になるのでしょうか? コンパイラは、デバッグ/セキュリティ/何らかの理由で、破棄されたオブジェクトのメモリをゼロにするか、最適化などとして別のオブジェクトでそのメモリをリサイクルすることを選択できます。実装は好きなように行うことができます。デストラクタを再度呼び出すことは、基本的に、任意の生メモリでメソッドを呼び出すことです。これは悪い考え (tm) です。

于 2010-05-05T09:44:07.613 に答える
8

標準での定式化の理由は、おそらく、他のすべてが非常に複雑になるためです。正確に二重削除が可能な場合(またはその逆)を定義する必要があります。つまり、単純なデストラクタまたは副作用を破棄できるデストラクタ。

一方、この動作には何のメリットもありません。実際には、クラスのデストラクタが上記の基準に適合するかどうかを一般的に知ることができないため、それから利益を得ることができません。これに依存する汎用コードはありません。そのようにバグを導入するのは非常に簡単です。そして最後に、それはどのように役立ちますか?オブジェクトの寿命を追跡しないずさんなコード、つまり指定不足のコードを書くことが可能になるだけです。標準がこれをサポートする必要があるのはなぜですか?


既存のコンパイラ/ランタイムは特定のコードを壊しますか? おそらくそうではありません – 不正なアクセスを防ぐための特別な実行時チェック (悪意のあるコードのように見えるものを防ぐため、または単に保護を漏らすため) がない限り。

于 2010-05-05T09:03:06.543 に答える
4

C++ の機能を使用してオブジェクトを作成および破棄する場合、そのオブジェクト モデルを使用することに同意したことになりますが、それは実装されています。

一部の実装は、他の実装よりも機密性が高い場合があります。たとえば、インタラクティブなインタープリター環境やデバッガーは、内省的であろうとする可能性があります。これには、二重破壊を具体的に警告することも含まれる場合があります.

一部のオブジェクトは、他のオブジェクトよりも複雑です。たとえば、仮想基底クラスを持つ仮想デストラクタは、やや複雑になる可能性があります。記憶が正しければ、一連の仮想デストラクタを実行すると、オブジェクトの動的な型が変化します。これは、最後に無効な状態に簡単につながる可能性があります。

コンストラクタとデストラクタを悪用する代わりに、適切な名前の関数を宣言して使用するのは簡単です。オブジェクト指向のストレート C は C++ でまだ可能であり、一部のジョブには適切なツールである可能性があります。いずれにせよ、デストラクタはすべての破壊関連のタスクに適した構成体ではありません。

于 2010-05-05T09:02:51.293 に答える
3

デストラクタは通常の関数ではありません。1 つを呼び出すと、1 つの関数が呼び出されるのではなく、多くの関数が呼び出されます。デストラクタの魔法です。どのように壊れるかを示すのを難しくするという唯一の目的で単純なデストラクタを提供しましたが、呼び出される他の関数が何をするかを示すことができませんでした。また、標準もそうではありません。それら機能では、物事が崩壊する可能性があります。

簡単な例として、デバッグ目的でオブジェクトの有効期間を追跡するコードをコンパイラが挿入するとします。コンストラクター [これは、ユーザーが要求したわけではないあらゆる種類のことを実行する魔法の関数でもあります] は、「ここにいます」というデータをどこかに格納します。デストラクタが呼び出される前に、そのデータを変更して「そこに行きます」と言います。デストラクタが呼び出されると、そのデータを見つけるために使用した情報が削除されます。したがって、次にデストラクタを呼び出すと、アクセス違反が発生します。

おそらく、仮想テーブルを含む例を考え出すこともできますが、サンプル コードには仮想関数が含まれていないため、ごまかすことになります。

于 2010-05-05T09:00:53.257 に答える
3

Classデストラクタを 2 回呼び出すと、私のマシンの Windows で次のようにクラッシュします。

class Class {
public:
    Class()
    {
        x = new int;
    }
    ~Class() 
    {
        delete x;
        x = (int*)0xbaadf00d;
    }

    int* x;
};

些細なデストラクタでクラッシュする実装を想像できます。たとえば、このような実装では、破壊されたオブジェクトが物理メモリから削除される可能性があり、それらへのアクセスは何らかのハードウェア障害につながります。Visual C++ はそのような種類の実装の 1 つではないように見えますが、誰にもわかりません。

于 2010-05-05T08:34:16.797 に答える
2

標準 12.4/14

オブジェクトに対してデストラクタが呼び出されると、そのオブジェクトは存在しなくなります。有効期間が終了したオブジェクトに対してデストラクタが呼び出された場合の動作は未定義です (3.8)。

このセクションは、削除によるデストラクタの呼び出しについて言及していると思います。言い換えれば、この段落の要点は、「オブジェクトを2回削除することは未定義の動作である」ということです。それが、コード例が正常に機能する理由です。

それにもかかわらず、この質問はかなり学術的です。デストラクタは、delete を介して呼び出されることを意図しています (sharptooth が正しく観察されているように、placement-new を介して割り当てられたオブジェクトを除いて)。デストラクタと 2 番目の関数の間でコードを共有する場合は、コードを別の関数に抽出し、それをデストラクタから呼び出すだけです。

于 2010-05-05T08:36:58.537 に答える
1

あなたが本当に求めているのは、コードが失敗するもっともらしい実装であるため、実装が便利なデバッグ モードを提供し、すべてのメモリ割り当てと、コンストラクターとデストラクターへのすべての呼び出しを追跡するとします。そのため、明示的なデストラクタ呼び出しの後、オブジェクトが破棄されたことを示すフラグを設定します。deleteこのフラグをチェックし、コードにバグの証拠が検出されるとプログラムを停止します。

コードを意図したとおりに「機能させる」ために、このデバッグ実装では、何もしないデストラクタを特殊なケースにし、そのフラグの設定をスキップする必要があります。つまり、デストラクタがたまたま何もしないためにバグを見つけられなかったが、誤って 2 回破棄したと仮定するのではなく、デストラクタが何もしない (と思う) ため、故意に2 回破棄していると仮定する必要があります。 . あなたが不注意か反逆者かのどちらかであり、反逆者に迎合するよりも、不注意な人々を助けるデバッグ実装の方がより多くのマイレージがあります;-)

于 2010-05-05T11:21:07.377 に答える
1

壊れる可能性のある実装の重要な例:

適合する C++ 実装は、ガベージ コレクションをサポートできます。これは長年の設計目標でした。GC は、dtor が実行されるとすぐにオブジェクトを GC できると想定する場合があります。したがって、各 dtor 呼び出しは、内部の GC ブックキーピングを更新します。dtor が同じポインターに対して 2 回目に呼び出されると、GC データ構造が破損する可能性があります。

于 2010-05-06T09:39:33.597 に答える
0

そうでない場合、オブジェクトがまだ生きているかどうかに関係なく、すべての実装が何らかのメタデータを介してブックマークする必要があるため、未定義です。基本的な C++ 設計規則に反するオブジェクトごとにそのコストを支払う必要があります。

于 2010-05-05T09:50:43.900 に答える
0

定義上、デストラクタはオブジェクトを「破棄」し、オブジェクトを 2 回破棄しても意味がありません。

あなたの例は機能しますが、一般的に機能するのは難しいです

于 2010-05-05T08:29:45.323 に答える
0

ほとんどの二重削除は危険であり、標準化委員会は、必要のない比較的少数のケースについて標準に例外を追加したくなかったため、未定義として分類されたと思います。

あなたのコードがどこで壊れる可能性があるかについて; 一部のコンパイラのデバッグ ビルドでコードの中断が見つかる場合があります。多くのコンパイラは、UB を、リリース モードでは「明確に定義された動作のパフォーマンスに影響を与えないことを行う」ものとして扱い、デバッグ ビルドでは「悪い動作を検出するためのチェックを挿入する」ものとして扱います。

于 2010-05-05T08:40:40.100 に答える
0

基本的に、既に指摘したように、デストラクタを 2 回呼び出すと、作業を実行するクラス デストラクタは失敗します。

于 2010-05-05T08:42:52.713 に答える
0

標準ではデストラクタの使用目的が明確にされており、デストラクタを誤って使用した場合にどうなるかを決定していないため、これは未定義の動作です。未定義の動作は、必ずしも「クラッシー スマッシュ」を意味するわけではありません。標準で定義されていないため、実装に任せているだけです。

私は C++ にあまり精通していませんが、デストラクタを単なる別のメンバー関数として扱うか、デストラクタが呼び出されたときに実際にオブジェクトを破棄するかのいずれかを実装することは歓迎されていると直感的に言えます。したがって、一部の実装では壊れるかもしれませんが、他の実装では壊れないかもしれません。誰が知っているか、それは未定義です(試してみると、鼻から飛び出す悪魔に気をつけてください).

于 2010-05-05T08:46:00.400 に答える
-1

その理由は、その規則がないと、プログラムの厳密性が低下するからです。コンパイル時に強制されなくても、より厳密であることは良いことです。その見返りとして、プログラムがどのように動作するかをより予測できるようになるからです。これは、クラスのソース コードが管理下にない場合に特に重要です。

多くの概念: RAII、スマート ポインター、および単なる一般的なメモリの割り当て/解放は、この規則に依存しています。デストラクタが呼び出される回数 (1 回) は、それらにとって不可欠です。したがって、そのようなことのドキュメントでは通常、次のように約束されています

そのようなルールがなければ、「C++ 言語のルールに従ってクラスを使用し、そのデストラクタを 2 回呼び出さないでください。そうすれば正しく動作します。」と述べているはずです。多くの仕様はそのように聞こえます。この概念は、言語にとって非常に重要であるため、標準ドキュメントではスキップされます。

これが理由です。バイナリ内部に関連するものではありません ( Potatoswatter の回答で説明されています)。

于 2010-05-05T09:41:42.923 に答える
-1

その理由は、クラスがたとえば参照カウントのスマート ポインターである可能性があるためです。したがって、デストラクタは参照カウンターをデクリメントします。そのカウンターが 0 になったら、実際のオブジェクトをクリーンアップする必要があります。

ただし、デストラクタを 2 回呼び出すと、カウントが台無しになります。

他の状況についても同じ考えです。おそらく、デストラクタはメモリの一部に 0 を書き込み、それを解放します (ユーザーのパスワードを誤ってメモリに残さないようにするため)。メモリの割り当てが解除された後に再度そのメモリに書き込もうとすると、アクセス違反が発生します。

オブジェクトが一度構築され、一度破棄されるのは理にかなっています。

于 2010-05-05T08:45:08.217 に答える