가끔 내 컴퓨터에서 "순수 가상 함수 호출"오류와 함께 충돌하는 프로그램을 발견합니다.
추상 클래스로 객체를 만들 수 없을 때 이러한 프로그램은 어떻게 컴파일합니까?
가끔 내 컴퓨터에서 "순수 가상 함수 호출"오류와 함께 충돌하는 프로그램을 발견합니다.
추상 클래스로 객체를 만들 수 없을 때 이러한 프로그램은 어떻게 컴파일합니까?
답변:
생성자 또는 소멸자에서 가상 함수 호출을 시도하면 결과가 발생할 수 있습니다. 생성자 또는 소멸자 (파생 클래스 개체가 생성되지 않았거나 이미 소멸됨)에서 가상 함수 호출을 할 수 없기 때문에 순수 가상 함수의 경우 기본 클래스 버전을 호출합니다. 존재하지 않습니다.
( 여기에서 라이브 데모보기 )
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에서 순수 가상 함수 호출을 얻을 수 있습니다. . 분명히 이것은 시도하고 수행하는 것은 매우 나쁜 일이지만 추상 클래스를 인터페이스로 사용하고 엉망이되면 볼 수 있습니다. 참조 카운트 된 인터페이스를 사용하고 있고 참조 카운트 버그가 있거나 다중 스레드 프로그램에서 객체 사용 / 객체 파괴 경쟁 조건이있는 경우 가능성이 더 높습니다. 이러한 종류의 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()
삭제 된 인스턴스의 메서드를 호출 할 때 일반적으로 발생하는 호출 은 기본 클래스가 최적화 (Microsoft 특정) 로 선언 된 경우 발생 하지 않습니다__declspec(novtable)
. 이를 통해 객체가 삭제 된 후 재정의 된 가상 메서드를 호출 할 수 있으며, 다른 형태로 물릴 때까지 문제를 가릴 수 있습니다. _purecall()
트랩은 당신의 친구입니다!
나는 파괴 된 객체로 인해 순수한 가상 함수가 호출되는 시나리오를 만났고 Len Holgate
이미 아주 좋은 대답을 가지고 있습니다. 예를 들어 몇 가지 색상을 추가하고 싶습니다.
Derived 클래스 소멸자는 vptr 포인트를 순수 가상 함수가있는 Base 클래스 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
메모리가 운영 체제로 반환되어 프로그램이 액세스 할 수 없기 때문에 간단히 a 를 얻을 수 있습니다. 따라서이 "순수한 가상 함수 호출"시나리오는 일반적으로 개체가 메모리 풀에 할당되고 개체가 삭제되는 동안 기본 메모리가 실제로 OS에 의해 회수되지 않고 프로세스에서 여전히 액세스 할 수있을 때 발생합니다.
어떤 내부 이유로 인해 추상 클래스에 대해 vtbl이 생성되고 (일종의 런타임 유형 정보에 필요할 수 있음) 무언가가 잘못되어 실제 개체가 가져옵니다. 버그입니다. 그것만으로도 일어날 수없는 일이 있다고 말해야합니다.
순수한 추측
편집 : 문제의 경우 내가 틀린 것 같습니다. OTOH IIRC 일부 언어는 생성자 소멸자에서 vtbl 호출을 허용합니다.
VS2010을 사용하고 공개 메서드에서 직접 소멸자를 호출하려고 할 때마다 런타임 중에 "순수한 가상 함수 호출"오류가 발생합니다.
template <typename T>
class Foo {
public:
Foo<T>() {};
~Foo<T>() {};
public:
void SomeMethod1() { this->~Foo(); }; /* ERROR */
};
그래서 ~ Foo () 안에있는 것을 private 메서드를 분리하도록 옮겼습니다. 그러면 매력처럼 작동했습니다.
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를 사용합니다.
추신. 원래 함수 호출은 [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();