D'où viennent les plantages des «appels de fonction virtuels purs»?

106

Je remarque parfois des programmes qui plantent sur mon ordinateur avec l'erreur: "appel de fonction virtuelle pure".

Comment ces programmes se compilent-ils même lorsqu'un objet ne peut pas être créé à partir d'une classe abstraite?

Brian R. Bondy
la source

Réponses:

107

Ils peuvent survenir si vous essayez d'effectuer un appel de fonction virtuelle à partir d'un constructeur ou d'un destructeur. Puisque vous ne pouvez pas faire un appel de fonction virtuelle à partir d'un constructeur ou d'un destructeur (l'objet de classe dérivé n'a pas été construit ou a déjà été détruit), il appelle la version de la classe de base, qui dans le cas d'une fonction virtuelle pure, ne n'existe pas.

(Voir la démo en direct ici )

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
la source
3
Une raison pour laquelle le compilateur n'a pas pu attraper cela, en général?
Thomas le
21
Dans le cas général, je ne peux pas l'attraper car le flux du ctor peut aller n'importe où et n'importe où peut appeler la fonction virtuelle pure. C'est le problème d'arrêt 101.
Shoosh
9
La réponse est légèrement fausse: une fonction virtuelle pure peut encore être définie, voir Wikipedia pour plus de détails.
Formulation
5
Je pense que cet exemple est trop simpliste: l' doIt()appel dans le constructeur est facilement dévirtualisé et distribué de Base::doIt()manière statique, ce qui provoque simplement une erreur de l'éditeur de liens. Ce dont nous avons vraiment besoin, c'est d'une situation dans laquelle le type dynamique lors d'une distribution dynamique est le type de base abstrait.
Kerrek SB
2
Cela peut être déclenché avec MSVC si vous ajoutez un niveau supplémentaire d'indirection: avoir Base::Baseappelé un non-virtuel f()qui à son tour appelle la doItméthode virtuelle (pure) .
Frerich Raabe
64

En plus du cas standard de l'appel d'une fonction virtuelle depuis le constructeur ou le destructeur d'un objet avec des fonctions virtuelles pures, vous pouvez également obtenir un appel de fonction virtuelle pure (au moins sur MSVC) si vous appelez une fonction virtuelle après que l'objet a été détruit . De toute évidence, c'est une très mauvaise chose à essayer, mais si vous travaillez avec des classes abstraites comme interfaces et que vous vous trompez, c'est quelque chose que vous pourriez voir. C'est peut-être plus probable si vous utilisez des interfaces comptées référencées et que vous avez un bogue de comptage de références ou si vous avez une condition de concurrence d'utilisation / destruction d'objet dans un programme multithread ... Le problème avec ces types d'appels purs est que c'est il est souvent moins facile de comprendre ce qui se passe car une vérification des «suspects habituels» des appels virtuels dans ctor et dtor sera claire.

Pour vous aider à déboguer ces types de problèmes, vous pouvez, dans différentes versions de MSVC, remplacer le gestionnaire d'appels purecall de la bibliothèque d'exécution. Vous faites cela en fournissant votre propre fonction avec cette signature:

int __cdecl _purecall(void)

et le lier avant de lier la bibliothèque d'exécution. Cela VOUS donne le contrôle de ce qui se passe lorsqu'un appel purecall est détecté. Une fois que vous avez le contrôle, vous pouvez faire quelque chose de plus utile que le gestionnaire standard. J'ai un gestionnaire qui peut fournir une trace de pile de l'endroit où le purecall s'est produit; voir ici: http://www.lenholgate.com/blog/2006/01/purecall.html pour plus de détails.

(Notez que vous pouvez également appeler _set_purecall_handler () pour installer votre gestionnaire dans certaines versions de MSVC).

Len Holgate
la source
1
Merci pour le pointeur sur l'obtention d'un appel _purecall () sur une instance supprimée; Je n'étais pas au courant de cela, mais je l'ai juste prouvé avec un petit code de test. En regardant un vidage post-mortem dans WinDbg, je pensais avoir affaire à une course où un autre thread essayait d'utiliser un objet dérivé avant qu'il n'ait été entièrement construit, mais cela jette un nouvel éclairage sur le problème et semble mieux correspondre aux preuves.
Dave Ruske
1
Une autre chose que j'ajouterai: l' _purecall()invocation qui se produit normalement lors de l'appel d'une méthode d'une instance supprimée ne se produira pas si la classe de base a été déclarée avec l' __declspec(novtable)optimisation (spécifique à Microsoft). Avec cela, il est tout à fait possible d'appeler une méthode virtuelle remplacée après la suppression de l'objet, ce qui pourrait masquer le problème jusqu'à ce qu'il vous mord sous une autre forme. Le _purecall()piège est votre ami!
Dave Ruske
C'est utile de connaître Dave, j'ai vu récemment quelques situations où je n'obtenais pas d'appels d'argent alors que je pensais que je devrais l'être. Peut-être que je tombais sous le coup de cette optimisation.
Len Holgate
1
@LenHolgate: réponse extrêmement précieuse. C'était EXACTEMENT notre cas problème (mauvais nombre de ref-count causé par les conditions de course). Merci beaucoup de nous avoir pointés dans la bonne direction (nous soupçonnions plutôt une corruption de v-table et
devenions
7

Habituellement, lorsque vous appelez une fonction virtuelle via un pointeur suspendu, l'instance a probablement déjà été détruite.

Il peut y avoir aussi des raisons plus «créatives»: peut-être avez-vous réussi à découper la partie de votre objet où la fonction virtuelle a été implémentée. Mais généralement, c'est simplement que l'instance a déjà été détruite.

Braden
la source
4

Je suis tombé sur le scénario selon lequel les fonctions virtuelles pures sont appelées à cause d'objets détruits, Len Holgatej'ai déjà une très bonne réponse , je voudrais ajouter de la couleur avec un exemple:

  1. Un objet dérivé est créé et le pointeur (en tant que classe de base) est enregistré quelque part
  2. L'objet dérivé est supprimé, mais le pointeur est toujours référencé
  3. Le pointeur qui pointe vers l'objet dérivé supprimé est appelé

Le destructeur de classe dérivée réinitialise les points vptr vers la classe de base vtable, qui a la fonction virtuelle pure, donc quand nous appelons la fonction virtuelle, il appelle en fait les fonctions virutales pures.

Cela peut se produire en raison d'un bogue de code évident ou d'un scénario compliqué de condition de concurrence dans les environnements multi-threading.

Voici un exemple simple (compile g ++ avec l'optimisation désactivée - un programme simple pourrait être facilement optimisé):

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

Et la trace de la pile ressemble à ceci:

#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

Surligner:

si l'objet est complètement supprimé, ce qui signifie que le destructeur est appelé et que memroy est récupéré, nous pouvons simplement obtenir un Segmentation faultcomme la mémoire est revenue au système d'exploitation, et le programme ne peut tout simplement pas y accéder. Donc, ce scénario "pur appel de fonction virtuelle" se produit généralement lorsque l'objet est alloué sur le pool de mémoire, alors qu'un objet est supprimé, la mémoire sous-jacente n'est en fait pas récupérée par le système d'exploitation, elle est toujours là accessible par le processus.

Baiyan Huang
la source
0

Je suppose qu'il y a un vtbl créé pour la classe abstraite pour une raison interne (il peut être nécessaire pour une sorte d'informations de type d'exécution) et que quelque chose ne va pas et qu'un objet réel l'obtient. C'est un bug. Cela seul devrait dire que quelque chose ne peut pas arriver.

Spéculation pure

edit: on dirait que je me trompe dans le cas en question. OTOH IIRC certains langages autorisent les appels vtbl hors du constructeur destructeur.

BCS
la source
Ce n'est pas un bogue dans le compilateur, si c'est ce que vous voulez dire.
Thomas le
Votre suspicion est juste - C # et Java le permettent. Dans ces langues, les bohjects en construction ont leur type final. En C ++, les objets changent de type pendant la construction et c'est pourquoi et quand vous pouvez avoir des objets avec un type abstrait.
MSalters
TOUTES les classes abstraites, et les objets réels créés à partir d'elles, ont besoin d'un vtbl (table de fonction virtuelle), répertoriant les fonctions virtuelles qui doivent être appelées dessus. En C ++, un objet est responsable de la création de ses propres membres, y compris la table de fonction virtuelle. Les constructeurs sont appelés de la classe de base à la classe dérivée, et les destructeurs sont appelés de la classe dérivée à la classe de base, de sorte que dans une classe de base abstraite, la table des fonctions virtuelles n'est pas encore disponible.
fuzzyTew
0

J'utilise VS2010 et chaque fois que j'essaye d'appeler destructor directement à partir de la méthode publique, j'obtiens une erreur "appel de fonction virtuelle pure" pendant l'exécution.

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

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

J'ai donc déplacé ce qu'il y a à l'intérieur de ~ Foo () pour séparer la méthode privée, puis cela a fonctionné comme un charme.

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

public:
  void _MethodThatDestructs() {};
  void SomeMethod1() { this->_MethodThatDestructs(); }; /* OK */
};
David Lee
la source
0

Si vous utilisez Borland / CodeGear / Embarcadero / Idera C ++ Builder, vous pouvez simplement implémenter

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

Pendant le débogage, placez un point d'arrêt dans le code et consultez la pile d'appels dans l'EDI, sinon enregistrez la pile d'appels dans votre gestionnaire d'exceptions (ou cette fonction) si vous disposez des outils appropriés pour cela. J'utilise personnellement MadExcept pour cela.

PS. L'appel de fonction d'origine se trouve dans [C ++ Builder] \ source \ cpprtl \ Source \ misc \ pureerr.cpp

Niki
la source
-2

Voici une façon sournoise pour que cela se produise. Cela m'est arrivé essentiellement aujourd'hui.

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 INFORMATIONS
la source
1
Au moins, il ne peut pas être reproduit sur mon vc2008, le vptr pointe vers la vtable de A lorsqu'il est initialisé pour la première fois dans le constructeur de A, mais lorsque B est complètement initialisé, le vptr est changé pour pointer vers la vtable de B, ce qui est ok
Baiyan Huang
coudnt le reproduire soit avec vs2010 / 12
makc
I had this essentially happen to me todayévidemment pas vrai, parce que tout simplement faux: une fonction virtuelle pure n'est appelée que lorsqu'elle callFoo()est appelée dans un constructeur (ou destructeur), car à ce moment l'objet est encore (ou déjà) au stade A. Voici une version en cours d'exécution de votre code sans l'erreur de syntaxe B b();- les parenthèses en font une déclaration de fonction, vous voulez un objet.
Wolf