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?
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?
Odpowiedzi:
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
}
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.
Base::Base
wywołanie niewirtualnego, f()
co z kolei wywołuje (czystą) doIt
metodę wirtualną .
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).
_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!
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.
Wpadłem na scenariusz, że czyste funkcje wirtualne są wywoływane z powodu zniszczonych obiektów, Len Holgate
mam już bardzo ładną odpowiedź , chciałbym dodać trochę koloru z przykładem:
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 fault
gdy 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.
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.
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 */
};
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
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();
I had this essentially happen to me today
oczywiś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.