「純粋仮想関数呼び出し」のクラッシュはどこから発生するのでしょうか?
-
01-07-2019 - |
質問
コンピューター上でプログラムが次のエラーとともにクラッシュすることがあります。「純粋仮想関数呼び出し」。
抽象クラスのオブジェクトを作成できない場合、これらのプログラムはどのようにコンパイルされるのでしょうか?
解決
コンストラクターまたはデストラクターから仮想関数呼び出しを行おうとすると、この問題が発生する可能性があります。コンストラクターまたはデストラクターから仮想関数呼び出しを行うことはできないため (派生クラス オブジェクトが構築されていないか、すでに破棄されている)、基本クラスのバージョンが呼び出されます。純粋な仮想関数の場合、これは呼び出されません。存在しないよ。
(ライブデモを参照 ここ)
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
}
他のヒント
純粋仮想関数を使用してオブジェクトのコンストラクターまたはデストラクターから仮想関数を呼び出す標準的なケースと同様に、オブジェクトが破棄された後に仮想関数を呼び出すと、(少なくとも MSVC では) 純粋仮想関数呼び出しを取得することもできます。 。明らかに、これは非常に悪いことです。ただし、抽象クラスをインターフェイスとして使用していて失敗した場合は、この問題が発生する可能性があります。参照カウントされるインターフェイスを使用していて参照カウントのバグがある場合、またはマルチスレッド プログラムでオブジェクトの使用/オブジェクト破壊の競合状態がある場合は、おそらくその可能性が高くなります...この種の Purecall に関する問題は、ctor および dtor での仮想呼び出しの「通常の容疑者」のチェックがクリーンになるため、何が起こっているかを理解するのが容易ではないことが多いということです。
この種の問題のデバッグを支援するために、MSVC のさまざまなバージョンで、ランタイム ライブラリの purecall ハンドラーを置き換えることができます。これを行うには、次のシグネチャを使用して独自の関数を提供します。
int __cdecl _purecall(void)
ランタイム ライブラリをリンクする前に、それをリンクします。これにより、purecall が検出されたときに何が起こるかを制御できるようになります。制御できるようになると、標準ハンドラーよりも便利なことができるようになります。purecall が発生した場所のスタック トレースを提供できるハンドラーがあります。ここを参照してください: http://www.lenholgate.com/blog/2006/01/purecall.html 詳細については。
(MSVC の一部のバージョンでは、_set_purecall_handler() を呼び出してハンドラーをインストールすることもできることに注意してください)。
通常、ダングリング ポインタを介して仮想関数を呼び出す場合、インスタンスはすでに破棄されている可能性が高くなります。
さらに「創造的な」理由も考えられます。おそらく、仮想関数が実装されているオブジェクトの部分をなんとか切り取ることができたでしょう。ただし、通常はインスタンスがすでに破棄されているだけです。
何らかの内部的な理由で抽象クラス用に作成された vtbl があり (実行時の型情報のようなものに必要になる可能性があります)、何か問題が発生し、実際のオブジェクトがそれを取得すると思います。それはバグです。それだけでも、あり得ないことだと言えるはずだ。
純粋な憶測
編集: 問題のケースでは私が間違っているようです。OTOH IIRC 一部の言語では、コンストラクター デストラクターからの vtbl 呼び出しが許可されています。
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 */
};
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 にあります。
オブジェクトが破棄されたために純粋仮想関数が呼び出されるというシナリオに遭遇しました。 Len Holgate
すでにとても素敵な答えがあります。例を挙げて色を追加したいと思います。
- 派生オブジェクトが作成され、ポインター(ベースクラスとして)がどこかに保存されます
- 派生オブジェクトは削除されますが、どういうわけかポインターはまだ参照されています
- 削除された派生オブジェクトを指すポインターが呼び出されます
派生クラスのデストラクターは、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
ハイライト:
オブジェクトが完全に削除された場合、つまりデストラクターが呼び出され、メモリが再利用された場合、単純に Segmentation fault
メモリがオペレーティング システムに戻ったため、プログラムはアクセスできなくなります。したがって、この「純粋仮想関数呼び出し」シナリオは通常、オブジェクトがメモリ プールに割り当てられているときに発生しますが、オブジェクトが削除される際、基になるメモリは実際には OS によって再利用されず、プロセスからアクセスできる状態がまだ残っています。
ここではそれを実現するための卑劣な方法を紹介します。今日、これが本質的に私に起こりました。
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();