"순수한 가상 함수 호출"충돌은 어디에서 발생합니까?


답변:


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
일반적으로 컴파일러가 이것을 잡을 수없는 이유는 무엇입니까?
Thomas

21
일반적인 경우 ctor의 흐름은 어디로 든 갈 수 있고 어디에서나 순수 가상 함수를 호출 할 수 있기 때문에 잡을 수 없습니다. 이것은 Halting 문제 101입니다.
shoosh

9
대답은 약간 잘못되었습니다. 순수 가상 기능이 여전히 정의되어있을 수 있습니다. 자세한 내용은 Wikipedia를 참조하십시오. 올바른 말씨는 : 없는
MSalters

5
이 예제는 너무 단순하다고 생각 doIt()합니다. 생성자 의 호출은 쉽게 비 가상화되고 Base::doIt()정적으로 전달되어 링커 오류가 발생합니다. 우리가 정말로 필요로하는 것은 동적 디스패치 중 동적 유형 이 추상 기본 유형 인 상황입니다.
Kerrek SB

2
간접 수준을 추가하면 MSVC로 트리거 될 수 있습니다. 즉, (순수한) 가상 메서드 를 차례로 호출 Base::Base하는 비가 상 f()을 호출해야합니다 doIt.
Frerich Raabe 2014 년

64

순수 가상 함수가있는 개체의 생성자 또는 소멸자에서 가상 함수를 호출하는 표준 사례뿐만 아니라 개체가 소멸 된 후 가상 함수를 호출하면 최소한 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의 일부 버전에 핸들러를 설치할 수도 있습니다.)


1
삭제 된 인스턴스에서 _purecall () 호출을 가져 오는 것에 대한 포인터를 주셔서 감사합니다. 나는 그것을 알지 못했지만 약간의 테스트 코드로 스스로 증명했습니다. WinDbg의 사후 덤프를 살펴보면 다른 스레드가 파생 된 개체가 완전히 생성되기 전에 사용하려고 시도하는 경주를 다루고 있다고 생각했지만 이것은 문제에 대한 새로운 빛을 비추고 증거에 더 잘 맞는 것처럼 보입니다.
Dave Ruske 15.06.06

1
추가 할 또 다른 사항 : _purecall()삭제 된 인스턴스의 메서드를 호출 할 때 일반적으로 발생하는 호출 은 기본 클래스가 최적화 (Microsoft 특정) 로 선언 된 경우 발생 하지 않습니다__declspec(novtable) . 이를 통해 객체가 삭제 된 후 재정의 된 가상 메서드를 호출 할 수 있으며, 다른 형태로 물릴 때까지 문제를 가릴 수 있습니다. _purecall()트랩은 당신의 친구입니다!
Dave Ruske 15.06.06

이것은 Dave를 아는 데 유용합니다. 최근에 내가 그래야한다고 생각했을 때 purecall을받지 못하는 몇 가지 상황을 보았습니다. 아마도 나는 그 최적화에 실패했을 것입니다.
Len Holgate 2015 년

1
@LenHolgate : 매우 귀중한 답변입니다. 이것은 정확히 우리의 문제 사례였습니다 (경쟁 조건으로 인한 잘못된 참조 횟수). 올바른 방향으로 우리를 가리키는 매우 감사합니다 (우리는 대신에 V-테이블 손상을 의심하고 범인 코드를 찾기 위해 노력 미쳐 가고 있었다)
BlueStrat

7

일반적으로 매달린 포인터를 통해 가상 함수를 호출 할 때 인스턴스가 이미 파괴되었을 가능성이 높습니다.

더 많은 "창의적인"이유도있을 수 있습니다. 가상 기능이 구현 된 개체의 일부를 잘라낼 수있었습니다. 그러나 일반적으로 인스턴스가 이미 파괴되었습니다.


4

나는 파괴 된 객체로 인해 순수한 가상 함수가 호출되는 시나리오를 만났고 Len Holgate이미 아주 좋은 대답을 가지고 있습니다. 예를 들어 몇 가지 색상을 추가하고 싶습니다.

  1. 파생 객체가 생성되고 포인터 (Base 클래스)가 어딘가에 저장됩니다.
  2. Derived 개체가 삭제되었지만 포인터는 여전히 참조됩니다.
  3. 삭제 된 Derived 객체를 가리키는 포인터가 호출됩니다.

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에 의해 회수되지 않고 프로세스에서 여전히 액세스 할 수있을 때 발생합니다.


0

어떤 내부 이유로 인해 추상 클래스에 대해 vtbl이 생성되고 (일종의 런타임 유형 정보에 필요할 수 있음) 무언가가 잘못되어 실제 개체가 가져옵니다. 버그입니다. 그것만으로도 일어날 수없는 일이 있다고 말해야합니다.

순수한 추측

편집 : 문제의 경우 내가 틀린 것 같습니다. OTOH IIRC 일부 언어는 생성자 소멸자에서 vtbl 호출을 허용합니다.


이것이 의미하는 바라면 컴파일러의 버그가 아닙니다.
Thomas

당신의 의심은 맞습니다-C #과 Java는 이것을 허용합니다. 이러한 언어에서 건설중인 bohjects에는 최종 유형이 있습니다. C ++에서 객체는 생성 중에 유형을 변경하므로 추상 유형의 객체를 가질 수있는 이유와시기입니다.
MSalters

모든 추상 클래스와 그로부터 파생 된 실제 개체에는 호출해야하는 가상 함수를 나열하는 vtbl (가상 함수 테이블)이 필요합니다. C ++에서 객체는 가상 함수 테이블을 포함하여 자체 멤버를 생성합니다. 생성자는 기본 클래스에서 파생으로 호출되고 소멸자는 파생에서 기본 클래스로 호출되므로 추상 기본 클래스에서 가상 함수 테이블을 아직 사용할 수 없습니다.
fuzzyTew

0

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 */
};

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를 사용합니다.

추신. 원래 함수 호출은 [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();. 괄호는 함수 선언으로 만들고 객체를 원합니다.
늑대
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.