Da dove vengono i crash della "chiamata di funzione virtuale pura"?


106

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:


107

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
}

3
Qualche motivo per cui il compilatore non è riuscito a catturarlo, in generale?
Thomas

21
Nel caso generale non è possibile catturarlo poiché il flusso dal ctor può andare ovunque e ovunque può chiamare la funzione virtuale pura. Questo è il problema di arresto 101.
shoosh

9
La risposta è leggermente sbagliata: si può ancora definire una funzione virtuale pura, vedere Wikipedia per i dettagli.
Fraseggio

5
Penso che questo esempio sia troppo semplicistico: la 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.
Kerrek SB

2
Questo può essere attivato con MSVC se aggiungi un ulteriore livello di riferimento indiretto: Base::Basechiama un non virtuale f()che a sua volta chiama il doItmetodo virtuale (puro) .
Frerich Raabe

64

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).


1
Grazie per il puntatore su come ottenere una chiamata _purecall () su un'istanza eliminata; Non ne ero a conoscenza, ma l'ho provato a me stesso con un piccolo codice di prova. Guardando un dump post-mortem in WinDbg, pensavo di avere a che fare con una gara in cui un altro thread stava cercando di utilizzare un oggetto derivato prima che fosse stato completamente costruito, ma questo getta una nuova luce sulla questione e sembra adattarsi meglio alle prove.
Dave Ruske

1
Un'altra cosa aggiungerò: l' _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!
Dave Ruske

È utile conoscere Dave, ho visto alcune situazioni di recente in cui non ricevevo purecall quando pensavo di doverlo essere. Forse stavo fallendo in quell'ottimizzazione.
Len Holgate

1
@ LenHolgate: risposta estremamente preziosa. Questo era ESATTAMENTE il nostro caso problematico (ref-count errato causato dalle condizioni di gara). Grazie mille per averci indicato la giusta direzione (sospettavamo invece la corruzione della tabella V e impazzivamo cercando di trovare il codice colpevole)
BlueStrat

7

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.


4

Mi sono imbattuto nello scenario in cui le funzioni virtuali pure vengono chiamate a causa di oggetti distrutti, Len Holgateho già una risposta molto carina , vorrei aggiungere un po 'di colore con un esempio:

  1. Viene creato un oggetto derivato e il puntatore (come classe Base) viene salvato da qualche parte
  2. L'oggetto derivato viene eliminato, ma in qualche modo si fa ancora riferimento al puntatore
  3. Viene chiamato il puntatore che punta all'oggetto derivato eliminato

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 faultquando 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.


0

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.


Non è un bug nel compilatore, se è questo che intendi.
Thomas

Il tuo sospetto è giusto: C # e Java lo consentono. In quelle lingue, i bohject in costruzione hanno il loro tipo finale. In C ++, gli oggetti cambiano tipo durante la costruzione ed è per questo e quando puoi avere oggetti con un tipo astratto.
MSalters

TUTTE le classi astratte e gli oggetti reali creati da esse hanno bisogno di una vtbl (tabella delle funzioni virtuali), che elenca le funzioni virtuali che dovrebbero essere chiamate su di essa. In C ++ un oggetto è responsabile della creazione dei propri membri, inclusa la tabella delle funzioni virtuali. I costruttori vengono chiamati dalla classe base alla classe derivata, mentre i distruttori vengono chiamati dalla classe derivata alla classe base, quindi in una classe base astratta la tabella delle funzioni virtuali non è ancora disponibile.
FuzzyTew

0

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

0

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


-2

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

1
Almeno non può essere riprodotto sul mio vc2008, il vptr punta a vtable di A quando viene inizializzato per la prima volta nel costruttore di A, ma poi quando B è completamente inizializzato, vptr viene modificato per puntare a vtable di B, il che è ok
Baiyan Huang

coudnt riprodurlo con vs2010 / 12
makc

I had this essentially happen to me todayovviamente 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.
Wolf,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.