A volte noto programmi che si bloccano sul mio computer con l'errore: "chiamata di funzione virtuale pura".
Come fanno questi programmi a compilare anche quando un oggetto non può essere creato da una classe astratta?
A volte noto programmi che si bloccano sul mio computer con l'errore: "chiamata di funzione virtuale pura".
Come fanno questi programmi a compilare anche quando un oggetto non può essere creato da una classe astratta?
Risposte:
Possono verificarsi se si tenta di effettuare una chiamata a una funzione virtuale da un costruttore o distruttore. Poiché non è possibile effettuare una chiamata a una funzione virtuale da un costruttore o distruttore (l'oggetto della classe derivata non è stato costruito o è già stato distrutto), chiama la versione della classe base, che nel caso di una funzione virtuale pura, non non esistono.
(Guarda la demo live qui )
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()
chiamata nel costruttore viene facilmente devirtualizzata e inviata a Base::doIt()
staticamente, il che causa solo un errore del linker. Ciò di cui abbiamo veramente bisogno è una situazione in cui il tipo dinamico durante un invio dinamico è il tipo di base astratto.
Base::Base
chiama un non virtuale f()
che a sua volta chiama il doIt
metodo virtuale (puro) .
Oltre al caso standard di chiamare una funzione virtuale dal costruttore o distruttore di un oggetto con funzioni virtuali pure puoi anche ottenere una chiamata di funzione virtuale pura (almeno su MSVC) se chiami una funzione virtuale dopo che l'oggetto è stato distrutto . Ovviamente questa è una cosa piuttosto brutta da provare e fare, ma se stai lavorando con classi astratte come interfacce e sbagli, allora è qualcosa che potresti vedere. È forse più probabile se stai usando interfacce conteggiate referenziate e hai un bug di conteggio ref o se hai una condizione di gara di uso / distruzione oggetto in un programma multi-thread ... La cosa su questi tipi di purecall è che è spesso è meno facile capire cosa sta succedendo in quanto un controllo per i "soliti sospetti" di chiamate virtuali in ctor e dtor verrà fuori pulito.
Per aiutare con il debug di questo tipo di problemi è possibile, in varie versioni di MSVC, sostituire il purecall handler della libreria runtime. Puoi farlo fornendo la tua funzione con questa firma:
int __cdecl _purecall(void)
e collegandolo prima di collegare la libreria runtime. Questo ti dà il controllo di ciò che accade quando viene rilevata una purecall. Una volta che hai il controllo, puoi fare qualcosa di più utile del gestore standard. Ho un gestore che può fornire una traccia dello stack di dove è avvenuta la purecall; vedere qui: http://www.lenholgate.com/blog/2006/01/purecall.html per maggiori dettagli.
(Nota che puoi anche chiamare _set_purecall_handler () per installare il tuo gestore in alcune versioni di MSVC).
_purecall()
invocazione che normalmente si verifica chiamando un metodo di un'istanza eliminata non avverrà se la classe base è stata dichiarata con l' __declspec(novtable)
ottimizzazione (specifica di Microsoft). Con ciò, è del tutto possibile chiamare un metodo virtuale sovrascritto dopo che l'oggetto è stato eliminato, il che potrebbe mascherare il problema finché non ti morde in qualche altra forma. La _purecall()
trappola è tua amica!
Di solito quando chiami una funzione virtuale tramite un puntatore penzolante, molto probabilmente l'istanza è già stata distrutta.
Ci possono essere anche ragioni più "creative": forse sei riuscito a tagliare la parte del tuo oggetto in cui è stata implementata la funzione virtuale. Ma di solito è solo che l'istanza è già stata distrutta.
Mi sono imbattuto nello scenario in cui le funzioni virtuali pure vengono chiamate a causa di oggetti distrutti, Len Holgate
ho già una risposta molto carina , vorrei aggiungere un po 'di colore con un esempio:
Il distruttore della classe Derived reimposta i punti vptr alla classe Base vtable, che ha la funzione virtuale pura, quindi quando chiamiamo la funzione virtuale, in realtà chiama quelle pure virutali.
Ciò potrebbe accadere a causa di un bug evidente del codice o di uno scenario complicato di race condition in ambienti multi-threading.
Ecco un semplice esempio (compilazione g ++ con ottimizzazione disattivata - un semplice programma potrebbe essere facilmente ottimizzato):
#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();
}
E la traccia dello stack ha il seguente aspetto:
#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
Evidenziare:
se l'oggetto viene completamente cancellato, il che significa che il distruttore viene chiamato e memroy viene recuperato, potremmo semplicemente ottenere un Segmentation fault
quando la memoria è tornata al sistema operativo e il programma non può accedervi. Quindi questo scenario di "chiamata di funzione virtuale pura" di solito si verifica quando l'oggetto viene allocato nel pool di memoria, mentre un oggetto viene eliminato, la memoria sottostante non viene effettivamente recuperata dal sistema operativo, ma è ancora accessibile dal processo.
Immagino che ci sia un vtbl creato per la classe astratta per qualche motivo interno (potrebbe essere necessario per una sorta di informazioni sul tipo di runtime) e qualcosa va storto e un oggetto reale lo ottiene. È un bug. Questo da solo dovrebbe dire che qualcosa che non può accadere lo è.
Pura speculazione
modifica: sembra che mi sbaglio nel caso in questione. OTOH IIRC alcuni linguaggi consentono chiamate vtbl dal distruttore del costruttore.
Uso VS2010 e ogni volta che provo a chiamare il distruttore direttamente dal metodo pubblico, ottengo un errore di "chiamata di funzione virtuale pura" durante il runtime.
template <typename T>
class Foo {
public:
Foo<T>() {};
~Foo<T>() {};
public:
void SomeMethod1() { this->~Foo(); }; /* ERROR */
};
Quindi ho spostato il contenuto di ~ Foo () in un metodo privato separato, quindi ha funzionato a meraviglia.
template <typename T>
class Foo {
public:
Foo<T>() {};
~Foo<T>() {};
public:
void _MethodThatDestructs() {};
void SomeMethod1() { this->_MethodThatDestructs(); }; /* OK */
};
Se usi Borland / CodeGear / Embarcadero / Idera C ++ Builder, puoi semplicemente implementare
extern "C" void _RTLENTRY _pure_error_()
{
//_ErrorExit("Pure virtual function called");
throw Exception("Pure virtual function called");
}
Durante il debug, posiziona un punto di interruzione nel codice e vedi lo stack di chiamate nell'IDE, altrimenti registra lo stack di chiamate nel gestore delle eccezioni (o in quella funzione) se hai gli strumenti appropriati per questo. Io personalmente uso MadExcept per quello.
PS. La chiamata alla funzione originale si trova in [C ++ Builder] \ source \ cpprtl \ Source \ misc \ pureerr.cpp
Ecco un modo subdolo perché accada. Questo essenzialmente mi è successo oggi.
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
ovviamente non è vero, perché semplicemente sbagliato: una funzione virtuale pura viene chiamata solo quando callFoo()
viene chiamata all'interno di un costruttore (o distruttore), perché in questo momento l'oggetto è ancora (o già) allo stadio A. Ecco una versione in esecuzione del tuo codice senza l'errore di sintassi in B b();
: le parentesi lo rendono una dichiarazione di funzione, vuoi un oggetto.