「純粋な仮想関数呼び出し」のクラッシュはどこから発生しますか?


106

「ピュア仮想関数呼び出し」というエラーでコンピューター上でクラッシュするプログラムに気付くことがあります。

抽象クラスのオブジェクトを作成できない場合、これらのプログラムはどのようにコンパイルされますか?

回答:


107

これらは、コンストラクターまたはデストラクターから仮想関数呼び出しを行おうとすると発生する可能性があります。コンストラクターまたはデストラクタから仮想関数を呼び出すことはできないため(派生クラスオブジェクトが構築されていないか、すでに破棄されている)、基本クラスバージョンを呼び出します。これは、純粋な仮想関数の場合、存在しません。

(こちらのライブデモをご覧ください

class Base
{
public:
    Base() { doIt(); }  // DON'T DO THIS
    virtual void doIt() = 0;
};

void Base::doIt()
{
    std::cout<<"Is it fine to call pure virtual function from constructor?";
}

class Derived : public Base
{
    void doIt() {}
};

int main(void)
{
    Derived d;  // This will cause "pure virtual function call" error
}

3
一般に、コンパイラがこれをキャッチできなかった理由は何ですか?
トーマス

21
一般的なケースでは、それをキャッチできません。これは、ctorからのフローがどこにでも行き、どこでも純粋仮想関数を呼び出すことができるためです。これは、停止問題101です
shoosh

9
答えは少し間違っています。純粋な仮想関数がまだ定義されている可能性があります。詳細はWikipediaを参照してください。正しい表現:存在しない可能性があります
MSalters '19 / 09/19

5
この例は単純すぎると思います。doIt()コンストラクターの呼び出しは簡単に仮想化解除され、Base::doIt()静的にディスパッチされます。これにより、リンカーエラーが発生します。本当に必要なのは、動的ディスパッチ中の動的タイプが抽象基本タイプである状況です。
Kerrek SB 2012

2
これは、追加のレベルの間接参照を追加する場合、MSVCでトリガーできます。Base::Base非仮想f()を呼び出して、(純粋な)仮想doItメソッドを呼び出します。
Frerich Raabe 2014

64

純粋な仮想関数を持つオブジェクトのコンストラクタまたはデストラクタから仮想関数を呼び出す標準的な場合と同様に、オブジェクトが破棄された後に仮想関数を呼び出すと、(少なくともMSVCで)純粋な仮想関数を呼び出すこともできます。 。明らかにこれは試してみるのはかなり悪いことですが、抽象クラスをインターフェイスとして使用していて、失敗した場合、それは目に見えるものです。参照カウントインターフェイスを使用していて、ref countバグがある場合、またはマルチスレッドプログラムでオブジェクト使用/オブジェクト破棄の競合状態が発生している可能性が高いです...これらの種類のpurecallの問題は、多くの場合、ctorとdtorの仮想呼び出しの「通常の容疑者」のチェックとして、何が起こっているのかを理解するのは簡単ではありません。

これらの種類の問題のデバッグに役立つように、MSVCのさまざまなバージョンで、ランタイムライブラリのpurecallハンドラーを置き換えることができます。これを行うには、次のシグネチャで独自の関数を提供します。

int __cdecl _purecall(void)

ランタイムライブラリをリンクする前にリンクします。これにより、purecallが検出されたときに何が起こるかを制御できます。コントロールを取得すると、標準ハンドラーよりも便利なことができます。purecallが発生した場所のスタックトレースを提供できるハンドラーがあります。詳細については、http//www.lenholgate.com/blog/2006/01/purecall.html参照してください。

(_set_purecall_handler()を呼び出して、一部のバージョンのMSVCにハンドラーをインストールすることもできます)。


1
削除されたインスタンスでの_purecall()呼び出しの取得についてのポインタに感謝します。私はそれに気づいていませんでしたが、小さなテストコードを使って自分で証明しただけです。WinDbgの死後のダンプを見ると、派生オブジェクトが完全に構築される前に別のスレッドが派生オブジェクトを使用しようとしたレースに対処していると思いましたが、これは問題に新たな光を当て、証拠によりよく適合しているようです。
Dave Ruske、2015年

1
もう1つ追加します。基本クラスが最適化(Microsoft固有)で宣言されている場合、_purecall()削除されたインスタンスのメソッドを呼び出すときに通常発生する呼び出しは行われませ__declspec(novtable)。これにより、オブジェクトが削除された後にオーバーライドされた仮想メソッドを呼び出すことが完全に可能になり、他の何らかの形で噛まれるまで問題を隠すことができます。_purecall()トラップはあなたの友達です!
Dave Ruske、2015年

それはデイブを知るのに役立ちます、私が私があるべきだと思ったときに私がpurecallsを取得していなかったいくつかの状況を最近見ました。おそらく、私はその最適化に失敗していました。
Len Holgate、2015年

1
@LenHolgate:非常に貴重な答え。これはまさに私たちの問題のケースでした(競合状態によって引き起こされた間違った参照カウント)。私たちを正しい方向に向けてくれてありがとう(私たちは代わりにvテーブルの破損を疑っていて、原因のコードを見つけようと狂っていました)
BlueStrat

7

通常、宙ぶらりんのポインターを介して仮想関数を呼び出すとき-ほとんどの場合、インスタンスはすでに破棄されています。

さらに「創造的」な理由もある可能性があります。おそらく、仮想関数が実装されたオブジェクトの部分を切り取ることができた可能性があります。しかし通常は、インスタンスがすでに破壊されているだけです。


4

オブジェクトが破壊されたために純粋な仮想関数が呼び出されるというシナリオに出くわしました。Len Holgateすでに非常に良い答えがあります。例を使って色を追加したいと思います。

  1. 派生オブジェクトが作成され、ポインター(基本クラスとして)がどこかに保存されます
  2. Derivedオブジェクトは削除されますが、どういうわけかポインタはまだ参照されています
  3. 削除されたDerivedオブジェクトを指すポインターが呼び出されます

Derivedクラスデストラクタは、vptrポイントを純粋な仮想関数を持つ基本クラスvtableにリセットするため、仮想関数を呼び出すと、実際には純粋な仮想関数が呼び出されます。

これは、明らかなコードのバグ、またはマルチスレッド環境での競合状態の複雑なシナリオが原因で発生する可能性があります。

これは簡単な例です(最適化をオフにしたg ++コンパイル-簡単なプログラムは簡単に最適化できます):

 #include <iostream>
 using namespace std;

 char pool[256];

 struct Base
 {
     virtual void foo() = 0;
     virtual ~Base(){};
 };

 struct Derived: public Base
 {
     virtual void foo() override { cout <<"Derived::foo()" << endl;}
 };

 int main()
 {
     auto* pd = new (pool) Derived();
     Base* pb = pd;
     pd->~Derived();
     pb->foo();
 }

スタックトレースは次のようになります。

#0  0x00007ffff7499428 in __GI_raise (sig=sig@entry=6) at ../sysdeps/unix/sysv/linux/raise.c:54
#1  0x00007ffff749b02a in __GI_abort () at abort.c:89
#2  0x00007ffff7ad78f7 in ?? () from /usr/lib/x86_64-linux-gnu/libstdc++.so.6
#3  0x00007ffff7adda46 in ?? () from /usr/lib/x86_64-linux-gnu/libstdc++.so.6
#4  0x00007ffff7adda81 in std::terminate() () from /usr/lib/x86_64-linux-gnu/libstdc++.so.6
#5  0x00007ffff7ade84f in __cxa_pure_virtual () from /usr/lib/x86_64-linux-gnu/libstdc++.so.6
#6  0x0000000000400f82 in main () at purev.C:22

ハイライト:

オブジェクトが完全に削除された場合、つまりデストラクタが呼び出されてmemroyが再利用さSegmentation faultれた場合、メモリがオペレーティングシステムに返され、プログラムがアクセスできないため、単にを取得する可能性があります。したがって、この「純粋な仮想関数呼び出し」のシナリオは通常、オブジェクトがメモリプールに割り当てられたときに発生しますが、オブジェクトが削除されても、基になるメモリは実際にはOSによって回収されず、プロセスからアクセスできます。


0

何らかの内部的な理由で(ある種のランタイムタイプ情報に必要になる可能性がある)抽象クラス用に作成されたvtblがあり、何かがうまくいかず、実際のオブジェクトがそれを取得すると思います。それはバグです。それだけでは起こり得ないことがあると言っておくべきです。

純粋な憶測

編集:問題のケースでは間違っているようです。OTOH IIRCの一部の言語では、コンストラクターデストラクタからのvtbl呼び出しを許可しています。


それがあなたの言っていることなら、それはコンパイラのバグではありません。
トーマス

あなたの疑惑は正しいです-C#とJavaはこれを許可します。これらの言語では、構築中のオブジェクトは最終的な型を持っています。C ++では、オブジェクトは構築中に型を変更します。そのため、抽象型のオブジェクトを持つことができます。
MSalters 2008

すべての抽象クラス、およびそれらから派生して作成された実際のオブジェクトには、vtbl(仮想関数テーブル)が必要で、どの仮想関数を呼び出す必要があるかがリストされています。C ++では、オブジェクトは、仮想関数テーブルを含む独自のメンバーを作成する責任があります。コンストラクタは基本クラスから派生クラスに呼び出され、デストラクタは派生クラスから基本クラスに呼び出されるため、抽象基本クラスでは仮想関数テーブルはまだ利用できません。
fuzzyTew 2009

0

私はVS2010を使用しており、パブリックメソッドからデストラクタを直接呼び出そうとすると、実行時に「純粋な仮想関数呼び出し」エラーが発生します。

template <typename T>
class Foo {
public:
  Foo<T>() {};
  ~Foo<T>() {};

public:
  void SomeMethod1() { this->~Foo(); }; /* ERROR */
};

だから私は〜Foo()の中にあるものを分離してプライベートメソッドに移動し、それが魅力のように機能しました。

template <typename T>
class Foo {
public:
  Foo<T>() {};
  ~Foo<T>() {};

public:
  void _MethodThatDestructs() {};
  void SomeMethod1() { this->_MethodThatDestructs(); }; /* OK */
};

0

Borland / CodeGear / Embarcadero / Idera C ++ Builderを使用している場合は、

extern "C" void _RTLENTRY _pure_error_()
{
    //_ErrorExit("Pure virtual function called");
    throw Exception("Pure virtual function called");
}

デバッグ中にコードにブレークポイントを配置し、IDEでコールスタックを確認します。それ以外の場合は、適切なツールがあれば、コールスタックを例外ハンドラー(またはその関数)に記録します。私はそのためにMadExceptを個人的に使用しています。

PS。元の関数呼び出しは[C ++ Builder] \ source \ cpprtl \ Source \ misc \ pureerr.cppにあります


-2

ここにそれが起こるための卑劣な方法があります。私はこれを本質的に今日私に起こさせました。

class A
{
  A *pThis;
  public:
  A()
   : pThis(this)
  {
  }

  void callFoo()
  {
    pThis->foo(); // call through the pThis ptr which was initialized in the constructor
  }

  virtual void foo() = 0;
};

class B : public A
{
public:
  virtual void foo()
  {
  }
};

B b();
b.callFoo();

1
少なくともそれは私のvc2008では再現できません。vptrは、Aのコンストラクターで最初に初期化されるときにAのvtableを指しますが、Bが完全に初期化されると、vptrはBのvtableを指すように変更されます。これは問題ありません
Baiyan Huang

coudntはvs2010 / 12のいずれかでそれを再現します
makc

I had this essentially happen to me today明らかに正しくありません。単に間違っているからです。純粋な仮想関数はcallFoo()、コンストラクタ(またはデストラクタ)内で呼び出されたときにのみ呼び出されます。現時点では、オブジェクトはまだ(または既に)Aステージにあるためです。ここに構文エラーのないコードの実行バージョンがありますB b();-括弧はそれを関数宣言にし、オブジェクトが必要です。
Wolf
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.