web-dev-qa-db-ja.com

デストラクタから仮想関数を呼び出す

これは安全ですか?

class Derived:  public PublicBase, private PrivateBase
{
 ... 

   ~Derived()
   {
      FunctionCall();
   }

   virtual void FunctionCall()
   {
      PrivateBase::FunctionCall();
   }
}

class PublicBase
{
   virtual ~PublicBase(){};
   virtual void FunctionCall() = 0;
}

class PrivateBase
{
   virtual ~PrivateBase(){};
   virtual void FunctionCall()
   {
    ....
   }
}


PublicBase* ptrBase = new Derived();
delete ptrBase;

このコードは、不正なアドレスのIPで(=-/// =)sometimesになります。

コンストラクターで仮想関数を呼び出すことは、誰にとっても明らかです。

http://www.artima.com/cppsource/nevercall.html のような記事から:デストラクタも仮想関数を呼び出すのにあまり良い場所ではないことを理解しています。

私の質問は「これは本当ですか?」です。 VS2010とVS2005でテストし、PrivateBase :: FunctionCallを呼び出しました。未定義の動作ですか?

36
cprogrammer

ここではフローに逆らうつもりですが...最初に、PublicBaseデストラクタが仮想であると仮定する必要があります。そうでなければ、Derivedデストラクタは呼び出されません。

コンストラクタ/デストラクタから仮想関数を呼び出すことは、通常はお勧めできません

この理由は、これら2つの操作中に動的ディスパッチが奇妙だからです。オブジェクトの実際のタイプchanges構築中、およびchanges破壊中。デストラクタが実行されているとき、オブジェクトはまさにその型であり、派生した型ではありません。動的ディスパッチは常に有効ですが、仮想関数のfinal overriderは、階層のどこにいるかによって変わります。

つまり、コンストラクター/デストラクターの仮想関数の呼び出しは、実行されるコンストラクター/デストラクターの型から派生した任意の型で実行されることを決して期待しないでください。

しかし

特定のケースでは、final overrider(少なくとも階層のこの部分)はabove your levelです。さらに、あなたは動的ディスパッチを使用していないです。呼び出しPrivateBase::FunctionCall();は静的に解決され、非仮想関数の呼び出しと実質的に同等です。関数がvirtualであるかどうかは、この呼び出しに影響しません。

yesあなたがやっているようにうまくやっていますが、ほとんどの人はルールの理由ではなくマントラを学ぶので、コードレビューでこれを説明することを強制されます。

これは安全ですか?

はい。コンストラクターまたはデストラクターから仮想関数を呼び出すと、オブジェクトの動的な型が現在構築または破棄されているものであるかのように関数がディスパッチされます。この場合、Derivedのデストラクタから呼び出されるため、Derived::FunctionCall(実際にはPrivateBase::FunctionCallを仮想的に呼び出さない)にディスパッチされます。これらはすべて明確に定義されています。

次の3つの理由から、コンストラクターまたはデストラクターから仮想関数を呼び出すことは「お勧めできません」:

  • 基本クラスから呼び出すと、(誤って)派生クラスのオーバーライドにディスパッチされると予期すると、予期しない動作が発生します。
  • 純粋な仮想の場合、未定義の動作が発生します。
  • それは常に間違っていると信じている人々にあなたの決定を説明し続けなければなりません。
20
Mike Seymour

一般に、ディスパッチされるクラスのオブジェクト(つまり、最も派生したクラスの「フル」オブジェクト)が完全に構築されていない限り、仮想関数を呼び出すことはお勧めできません。そして、これはそうではありません

  • すべてのコンストラクターの実行が完了するまで
  • デストラクタが実行を終了した後
2
Grzegorz Herman

スコットによると、これは非常に悪い考えです。 link

これは、破壊プロセスの理解を深めるためにコンパイルして実行したものです。

#include <iostream>
using namespace std;


class A {
public:
  virtual void method() {
    cout << "A::method" << endl;
  }

  void otherMethod() {
    method();
  }

  virtual ~A() {
    cout << "A::destructor" << endl;
    otherMethod();
  }

};

class B : public A {
public:
  virtual void method() {
    cout << "B::method" << endl;
  }

  virtual ~B() {
    cout << "B::destructor" << endl;
  }
};

int main() {

  A* a = new B();

  a->method();

  delete a;

}
1
sji