Skąd się biorą awarie „czysto wirtualnego wywołania funkcji”?

106

Czasami zauważam awarie programów na moim komputerze z błędem: „czyste wywołanie funkcji wirtualnej”.

Jak te programy nawet kompilują się, gdy nie można utworzyć obiektu z klasy abstrakcyjnej?

Brian R. Bondy
źródło

Odpowiedzi:

107

Mogą one powstać, jeśli spróbujesz wywołać funkcję wirtualną z konstruktora lub destruktora. Ponieważ nie można wywołać funkcji wirtualnej z konstruktora lub destruktora (obiekt klasy pochodnej nie został skonstruowany lub został już zniszczony), wywołuje ona wersję klasy bazowej, co w przypadku czystej funkcji wirtualnej nie nie istnieje.

(Zobacz demo na żywo tutaj )

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
}
Adam Rosenfield
źródło
3
Czy jest jakiś powód, dla którego kompilator nie mógł tego ogólnie złapać?
Thomas,
21
W ogólnym przypadku nie można tego złapać, ponieważ przepływ z ctora może iść wszędzie i wszędzie może wywołać czystą funkcję wirtualną. To jest problem z zatrzymaniem 101.
shoosh,
9
Odpowiedź jest nieco błędna: nadal można zdefiniować czystą funkcję wirtualną, szczegóły można znaleźć w Wikipedii. Prawidłowe sformułowanie: może nie istnieć
MSalters
5
Myślę, że ten przykład jest zbyt uproszczony: doIt()wywołanie w konstruktorze jest łatwo dewirtualizowane i wysyłane Base::doIt()statycznie, co po prostu powoduje błąd konsolidatora. To, czego naprawdę potrzebujemy, to sytuacja, w której typ dynamiczny podczas dynamicznej wysyłki jest abstrakcyjnym typem bazowym.
Kerrek SB,
2
Można to wywołać za pomocą MSVC, jeśli dodasz dodatkowy poziom pośrednictwa: Base::Basewywołanie niewirtualnego, f()co z kolei wywołuje (czystą) doItmetodę wirtualną .
Frerich Raabe
64

Oprócz standardowego wywołania funkcji wirtualnej z konstruktora lub destruktora obiektu z czystymi funkcjami wirtualnymi można również uzyskać wywołanie czystej funkcji wirtualnej (przynajmniej na MSVC), jeśli wywołasz funkcję wirtualną po zniszczeniu obiektu . Oczywiście jest to bardzo zła rzecz, ale jeśli pracujesz z klasami abstrakcyjnymi jako interfejsami i coś zepsujesz, możesz to zobaczyć. Jest to prawdopodobnie bardziej prawdopodobne, jeśli używasz odwołań zliczanych interfejsów i masz błąd liczby referencji lub jeśli masz warunek wyścigu użycie obiektu / zniszczenie obiektu w programie wielowątkowym ... Rzecz w tego rodzaju czystym wywołaniu polega na tym, że często trudniej jest odgadnąć, co się dzieje, jako że sprawdzanie „zwykłych podejrzanych” połączeń wirtualnych w ctor i dtor zostanie wyczyszczone.

Aby pomóc w debugowaniu tego rodzaju problemów, możesz w różnych wersjach MSVC zastąpić procedurę obsługi purecall biblioteki wykonawczej. Robisz to, udostępniając własną funkcję z tym podpisem:

int __cdecl _purecall(void)

i łączenie go przed połączeniem biblioteki wykonawczej. Daje to Tobie kontrolę nad tym, co się stanie, gdy zostanie wykryte czyste połączenie. Gdy już uzyskasz kontrolę, możesz zrobić coś bardziej użytecznego niż standardowa obsługa. Mam procedurę obsługi, która może zapewnić ślad stosu, gdzie wydarzyło się czyste wywołanie; zobacz tutaj: http://www.lenholgate.com/blog/2006/01/purecall.html, aby uzyskać więcej informacji.

(Zauważ, że możesz również wywołać _set_purecall_handler (), aby zainstalować program obsługi w niektórych wersjach MSVC).

Len Holgate
źródło
1
Dziękuję za wskazówkę dotyczącą uzyskania wywołania _purecall () na usuniętej instancji; Nie byłem tego świadomy, ale po prostu udowodniłem to sobie za pomocą małego kodu testowego. Patrząc na zrzut sekcji zwłok w WinDbg, pomyślałem, że mam do czynienia z rasą, w której inny wątek próbował użyć obiektu pochodnego, zanim został w pełni skonstruowany, ale rzuca to nowe światło na problem i wydaje się lepiej pasować do dowodów.
Dave Ruske
1
Jeszcze jedna rzecz, którą dodam: _purecall()wywołanie, które normalnie występuje przy wywołaniu metody usuniętej instancji, nie nastąpi, jeśli klasa bazowa została zadeklarowana z __declspec(novtable)optymalizacją (specyficzna dla firmy Microsoft). Dzięki temu jest całkowicie możliwe wywołanie zastąpionej metody wirtualnej po usunięciu obiektu, która może maskować problem, dopóki nie ugryzie Cię w innej formie. _purecall()Pułapka jest twoim przyjacielem!
Dave Ruske
Dobrze jest wiedzieć, Dave, ostatnio widziałem kilka sytuacji, w których nie otrzymywałem żadnych telefonów, a myślałem, że powinienem. Być może wpadłem w błąd tej optymalizacji.
Len Holgate
1
@LenHolgate: Niezwykle cenna odpowiedź. To był DOKŁADNIE nasz problem (zła liczba ref spowodowana warunkami wyścigu). Bardzo dziękujemy za skierowanie nas we właściwym kierunku (zamiast tego podejrzewaliśmy korupcję stołu v i oszaleliśmy, próbując znaleźć kod winowajcy)
BlueStrat
7

Zwykle gdy wywołujesz funkcję wirtualną za pomocą wiszącego wskaźnika - najprawdopodobniej instancja została już zniszczona.

Powody mogą być też bardziej „kreatywne”: może udało ci się odciąć część obiektu, w której zaimplementowano funkcję wirtualną. Ale zwykle po prostu instancja została już zniszczona.

Braden
źródło
4

Wpadłem na scenariusz, że czyste funkcje wirtualne są wywoływane z powodu zniszczonych obiektów, Len Holgatemam już bardzo ładną odpowiedź , chciałbym dodać trochę koloru z przykładem:

  1. Tworzony jest obiekt pochodny, a wskaźnik (jako klasa bazowa) jest zapisywany gdzieś
  2. Obiekt Derived jest usuwany, ale w jakiś sposób istnieje odwołanie do wskaźnika
  3. Zostanie wywołany wskaźnik wskazujący na usunięty obiekt pochodny

Destruktor klasy pochodnej resetuje punkty vptr do klasy podstawowej vtable, która ma czystą funkcję wirtualną, więc kiedy wywołujemy funkcję wirtualną, w rzeczywistości wywołuje ona funkcje czysto wirutalne.

Może się to zdarzyć z powodu oczywistego błędu w kodzie lub skomplikowanego scenariusza wyścigu w środowiskach wielowątkowych.

Oto prosty przykład (kompilacja g ++ z wyłączoną optymalizacją - prosty program można łatwo zoptymalizować na zewnątrz):

 #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();
 }

A ślad stosu wygląda następująco:

#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

Atrakcja:

jeśli obiekt zostanie całkowicie usunięty, co oznacza, że ​​zostanie wywołany destruktor, a pamięć zostanie odzyskana, możemy po prostu otrzymać a, Segmentation faultgdy pamięć wróciła do systemu operacyjnego, a program po prostu nie może uzyskać do niej dostępu. Tak więc ten scenariusz „czystego wywołania funkcji wirtualnej” zwykle ma miejsce, gdy obiekt jest alokowany w puli pamięci, podczas gdy obiekt jest usuwany, pamięć bazowa nie jest w rzeczywistości odzyskiwana przez system operacyjny i nadal jest dostępna dla procesu.

Baiyan Huang
źródło
0

Domyślam się, że istnieje vtbl utworzony dla klasy abstrakcyjnej z jakiegoś wewnętrznego powodu (może być potrzebny do jakiegoś rodzaju informacji o typie działania) i coś idzie nie tak i prawdziwy obiekt to dostaje. To błąd. Już samo to powinno powiedzieć, że coś, co nie może się zdarzyć, jest.

Czysta spekulacja

edycja: wygląda na to, że się mylę w omawianej sprawie. OTOH IIRC w niektórych językach zezwalają na wywołania vtbl z destruktora konstruktora.

BCS
źródło
To nie jest błąd w kompilatorze, jeśli o to ci chodzi.
Thomas,
Twoje podejrzenie jest słuszne - C # i Java na to pozwalają. W tych językach powstające projekty mają swój ostateczny typ. W C ++ obiekty zmieniają typ podczas konstrukcji i dlatego i kiedy możesz mieć obiekty z typem abstrakcyjnym.
MSalters,
WSZYSTKIE klasy abstrakcyjne i utworzone z nich rzeczywiste obiekty wymagają vtbl (wirtualnej tablicy funkcji), zawierającej listę funkcji wirtualnych, które powinny być na niej wywołane. W C ++ obiekt jest odpowiedzialny za tworzenie własnych składowych, w tym tabeli funkcji wirtualnych. Konstruktory są wywoływane z klasy bazowej do klasy pochodnej, a destruktory są wywoływane z klasy pochodnej do klasy bazowej, więc w abstrakcyjnej klasie bazowej tabela funkcji wirtualnych nie jest jeszcze dostępna.
fuzzyTew
0

Używam VS2010 i za każdym razem, gdy próbuję wywołać destruktor bezpośrednio z metody publicznej, podczas działania pojawia się błąd „czystego wywołania funkcji wirtualnej”.

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

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

Więc przeniosłem to, co jest w środku ~ Foo (), aby oddzielić metodę prywatną, a potem zadziałało jak urok.

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

public:
  void _MethodThatDestructs() {};
  void SomeMethod1() { this->_MethodThatDestructs(); }; /* OK */
};
David Lee
źródło
0

Jeśli używasz Borland / CodeGear / Embarcadero / Idera C ++ Builder, możesz po prostu zaimplementować

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

Podczas debugowania umieść punkt przerwania w kodzie i zobacz stos wywołań w IDE, w przeciwnym razie zarejestruj stos wywołań w programie obsługi wyjątków (lub tej funkcji), jeśli masz do tego odpowiednie narzędzia. Osobiście używam MadExcept do tego.

PS. Oryginalne wywołanie funkcji znajduje się w [C ++ Builder] \ source \ cpprtl \ Source \ misc \ pureerr.cpp

Niki
źródło
-2

Oto podstępny sposób, aby to się stało. Zasadniczo spotkałem się z tym dzisiaj.

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();
1800 INFORMACJE
źródło
1
Przynajmniej nie można go odtworzyć na moim vc2008, vptr wskazuje na vtable A podczas pierwszej inicjalizacji w konstruktorze A, ale potem, gdy B jest w pełni zainicjowany, vptr jest zmieniany tak, aby wskazywał na vtable B, co jest w porządku
Baiyan Huang
nie można go odtworzyć za pomocą vs2010 / 12
makc
I had this essentially happen to me todayoczywiście nieprawda, ponieważ po prostu błędna: czysta funkcja wirtualna jest wywoływana tylko wtedy, gdy callFoo()jest wywoływana w konstruktorze (lub destruktorze), ponieważ w tym momencie obiekt jest nadal (lub już) na etapie A. Oto działająca wersja twojego kodu bez błędu składniowego B b();- nawiasy sprawiają, że jest to deklaracja funkcji, chcesz obiekt.
Wilk