「ピュア仮想関数呼び出し」というエラーでコンピューター上でクラッシュするプログラムに気付くことがあります。
抽象クラスのオブジェクトを作成できない場合、これらのプログラムはどのようにコンパイルされますか?
「ピュア仮想関数呼び出し」というエラーでコンピューター上でクラッシュするプログラムに気付くことがあります。
抽象クラスのオブジェクトを作成できない場合、これらのプログラムはどのようにコンパイルされますか?
回答:
これらは、コンストラクターまたはデストラクターから仮想関数呼び出しを行おうとすると発生する可能性があります。コンストラクターまたはデストラクタから仮想関数を呼び出すことはできないため(派生クラスオブジェクトが構築されていないか、すでに破棄されている)、基本クラスバージョンを呼び出します。これは、純粋な仮想関数の場合、存在しません。
(こちらのライブデモをご覧ください)
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
}
doIt()
コンストラクターの呼び出しは簡単に仮想化解除され、Base::doIt()
静的にディスパッチされます。これにより、リンカーエラーが発生します。本当に必要なのは、動的ディスパッチ中の動的タイプが抽象基本タイプである状況です。
Base::Base
非仮想f()
を呼び出して、(純粋な)仮想doIt
メソッドを呼び出します。
純粋な仮想関数を持つオブジェクトのコンストラクタまたはデストラクタから仮想関数を呼び出す標準的な場合と同様に、オブジェクトが破棄された後に仮想関数を呼び出すと、(少なくとも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にハンドラーをインストールすることもできます)。
_purecall()
削除されたインスタンスのメソッドを呼び出すときに通常発生する呼び出しは行われません__declspec(novtable)
。これにより、オブジェクトが削除された後にオーバーライドされた仮想メソッドを呼び出すことが完全に可能になり、他の何らかの形で噛まれるまで問題を隠すことができます。_purecall()
トラップはあなたの友達です!
オブジェクトが破壊されたために純粋な仮想関数が呼び出されるというシナリオに出くわしました。Len Holgate
すでに非常に良い答えがあります。例を使って色を追加したいと思います。
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によって回収されず、プロセスからアクセスできます。
何らかの内部的な理由で(ある種のランタイムタイプ情報に必要になる可能性がある)抽象クラス用に作成された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にあります
ここにそれが起こるための卑劣な方法があります。私はこれを本質的に今日私に起こさせました。
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();