28

前方宣言で何をしてはいけないかを説明するために使用されるこの古典的な例を考えてみましょう:

//in Handle.h file
class Body;

class Handle
{
   public:
      Handle();
      ~Handle() {delete impl_;}
   //....
   private:
      Body *impl_;
};

//---------------------------------------
//in Handle.cpp file

#include "Handle.h"

class Body 
{
  //Non-trivial destructor here
    public:
       ~Body () {//Do a lot of things...}
};

Handle::Handle () : impl_(new Body) {}

//---------------------------------------
//in Handle_user.cpp client code:

#include "Handle.h"

//... in some function... 
{
    Handle handleObj;

    //Do smtg with handleObj...

    //handleObj now reaches end-of-life, and BUM: Undefined behaviour
} 

Bodyのデストラクタは自明ではないため、このケースはUBに向かっていることを標準から理解しています。私が理解しようとしているのは、これの根本的な原因です。

つまり、ハンドルの dtor がインラインであるという事実によって問題が「トリガー」されているように見えるため、コンパイラは次の「インライン展開」のようなことを行います (ここではほぼ疑似コード)。

inline Handle::~Handle()
{
     impl_->~Body();
     operator delete (impl_);
}

Handle インスタンスが破棄されるすべての翻訳単位 (Handle_user.cppこの場合のみ) で、そうですか? 私はこれを理解できません:わかりました、上記のインライン展開を生成するとき、コンパイラーは Body クラスの完全な定義を持っていませんが、なぜ単純にリンカにimpl_->~Body()物事を解決させて、Body のデストラクタを呼び出させることができないのでしょうか?実装ファイルで実際に定義されている関数?

言い換えれば、Handle 破壊の時点で、コンパイラは Body に対して (自明ではない) デストラクタが存在するかどうかさえわからないことを理解していますが、なぜいつものようにできないのでしょうか。リンカーが埋めるための「プレースホルダー」であり、その機能が実際に利用できない場合、最終的にリンカーは「未解決の外部」になりますか?

ここで何か大きなものを見逃していますか (その場合、愚かな質問で申し訳ありません)。そうでない場合は、この背後にある理論的根拠を理解したいと思っています。

4

6 に答える 6

31

いくつかの答えを組み合わせて自分の答えを追加するには、クラス定義がないと、呼び出し元のコードはわかりません。

  • クラスに宣言されたデストラクタがあるかどうか、またはデフォルトのデストラクタが使用されるかどうか、もしそうなら、デフォルトのデストラクタが些細なものであるかどうか、
  • デストラクタが呼び出し元のコードにアクセスできるかどうか、
  • どの基本クラスが存在し、デストラクタがありますか、
  • デストラクタが仮想かどうか。仮想関数呼び出しは、事実上、非仮想関数呼び出しとは異なる呼び出し規約を使用します。コンパイラは単に「〜Bodyを呼び出すコードを発行」することはできず、後で詳細を処理するためにリンカを離れることはできません。
  • (これはちょうど、GManに感謝します)deleteクラスのためにオーバーロードされているかどうか。

これらの理由の一部またはすべてのために、不完全な型でメンバー関数を呼び出すことはできません(さらに、デストラクタに適用されない別の機能-パラメータや戻り型がわかりません)。デストラクタも例外ではありません。ですから、「いつものようにできないのはなぜですか」と言ったときの意味がわかりません。

すでにご存知のように、解決策は、Handleの定義を持つTUでのデストラクタを定義することです。これは、の関数を呼び出す、またはのデータメンバーを使用するBody他のすべてのメンバー関数を定義するのと同じ場所です。次に、コンパイルされた時点で、その呼び出しのコードを発行するためにすべての情報が利用可能になります。HandleBodydelete impl_;

標準では実際には5.3.5/5と書かれていることに注意してください。

削除されるオブジェクトの削除時点でのクラスタイプが不完全であり、完全なクラスに重要なデストラクタまたは割り当て解除関数がある場合、動作は定義されていません。

これは、Cでできるのfreeと同じように、不完全なPODタイプを削除できるようにするためだと思います。ただし、g ++を試してみると、かなり厳しい警告が表示されます。

于 2010-03-25T16:38:42.403 に答える
7

デストラクタが公開されるかどうかはわかりません。

于 2010-03-25T16:27:09.773 に答える
6

仮想メソッドまたは非仮想メソッドの呼び出しは、2 つのまったく異なるものです。

非仮想メソッドを呼び出す場合、コンパイラはこれを行うコードを生成する必要があります。

  • すべての引数をスタックに置く
  • 関数を呼び出し、呼び出しを解決する必要があることをリンカーに伝えます

デストラクタについて話しているので、スタックに置く引数はありません。そのため、単純に呼び出しを実行して、リンカに呼び出しを解決するように指示できるように見えます。プロトタイプは必要ありません。

ただし、仮想メソッドの呼び出しはまったく異なります。

  • すべての引数をスタックに置く
  • インスタンスの vptr を取得します
  • vtable から n 番目のエントリを取得する
  • この n 番目のエントリが指す関数を呼び出す

これはまったく異なるため、コンパイラは、仮想メソッドと非仮想メソッドのどちらを呼び出しているかを実際に認識している必要があります。

2 番目に重要なことは、仮想メソッドが vtable 内のどの位置にあるかをコンパイラが知る必要があるということです。このためには、クラスの完全な定義も必要です。

于 2010-03-25T16:23:55.257 に答える
4

Bodyコードを適切に宣言しないと、デストラクタがアクセス可能かどうか (つまり、パブリック)Handle.hがわからない。virtual

于 2010-03-25T16:25:32.273 に答える
2

推測ですが、おそらくクラスごとの割り当て演算子の能力に関係しています。

あれは:

struct foo
{
    void* operator new(size_t);
    void operator delete(void*);
};

// in another header, like your example

struct foo;

struct bar
{
    bar();
    ~bar() { delete myFoo; }

    foo* myFoo;
};

// in translation unit

#include "bar.h"
#include "foo.h"

bar::bar() :
myFoo(new foo) // uses foo::operator new
{}

// but destructor uses global...!!

そして、割り当て演算子が一致せず、未定義の動作に入りました。それが起こらないことを保証する唯一の方法は、「型を完全にする」と言うことです。そうしないと確保できません。

于 2010-03-25T16:22:09.037 に答える
1

これは、メソッド (間接的にデストラクタ) を呼び出す特殊なケースにすぎません。delete impl_ は事実上、デストラクタを呼び出してから、適切な演算子 delete (グローバルまたはクラス) を呼び出すだけです。不完全な型に対して他の関数を呼び出すことはできません。では、デストラクタへの delete の呼び出しに特別な処理が与えられるのはなぜでしょうか?

私が確信していない部分は、メソッド呼び出しのように単に禁止するのではなく、どのような複雑さが標準を未定義にするのかということです。

于 2010-03-25T16:45:30.733 に答える