web-dev-qa-db-fra.com

D'où viennent les accidents "Appel de fonction virtuelle pure"?

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

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

102
Brian R. Bondy

Ils peuvent en résulter si vous essayez de faire un appel de fonction virtuelle à partir d'un constructeur ou d'un destructeur. Comme vous ne pouvez pas appeler de fonction virtuelle à partir d'un constructeur ou d'un destructeur (l'objet de classe dérivée 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. 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
}
104
Adam Rosenfield

En plus du cas standard d'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 la destruction de l'objet. . Évidemment, c'est une très mauvaise chose à faire, mais si vous utilisez des classes abstraites comme interfaces et que vous vous trompez, c'est quelque chose que vous pourriez voir. C'est probablement plus probable si vous utilisez des interfaces référencées et comptées et que vous avez un bogue ref count ou si vous avez une condition de concurrence d'objet/destruction d'objet dans un programme multithread ... Le problème avec ce type de purecall est que c'est Il est souvent moins facile de comprendre ce qui se passe, car une vérification des "suspects habituels" d'appels virtuels chez ctor et dtor sera irréprochable.

Pour faciliter le débogage de ce type de problèmes, vous pouvez, dans diverses versions de MSVC, remplacer le gestionnaire purecall de la bibliothèque d'exécution. Vous faites cela en fournissant votre propre fonction avec cette signature:

int __cdecl _purecall(void)

et en le liant avant de lier la bibliothèque d'exécution. Cela vous donne le contrôle de ce qui se passe quand un 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 passé; 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).

62
Len Holgate

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

Il peut également y avoir des raisons plus "créatives": peut-être avez-vous réussi à couper la partie de votre objet où la fonction virtuelle a été mise en œuvre. Mais généralement, c'est simplement que l'instance a déjà été détruite.

7
Braden

Je me suis heurté au scénario selon lequel les fonctions virtuelles pures sont appelées à cause d'objets détruits, Len Holgate ont déjà un très joli 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 sur l'objet dérivé supprimé est appelé

Le destructeur de classe dérivée réinitialise les points vptr sur la classe de base vtable, qui a la fonction virtuelle pure. Ainsi, lorsque nous appelons la fonction virtuelle, elle appelle en réalité les fonctions virtuelles pures.

Cela peut être dû à un bogue de code évident ou à un scénario compliqué de conditions de concurrence critique dans des environnements multithreads.

Voici un exemple simple (compiler g ++ avec 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

Mettez en surbrillance:

si l'objet est complètement supprimé, ce qui signifie que destructor est appelé et que memroy est récupéré, nous pouvons simplement obtenir un Segmentation fault car la mémoire est revenue dans le système d’exploitation et le programme ne peut tout simplement pas y accéder. Donc, ce scénario "appel de fonction virtuelle pure" 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, il est toujours accessible par le processus.

1
Baiyan Huang

J'utilise VS2010 et chaque fois que j'essaie d'appeler destructor directement à partir d'une méthode publique, l'erreur "Appel de fonction virtuelle pure" s'affiche lors de l'exécution.

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

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

J'ai donc déplacé le contenu de ~ Foo () à l'intérieur pour séparer la méthode privée, puis cela a fonctionné à merveille.

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

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

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 à cette fin. Personnellement, j'utilise MadExcept pour cela.

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

0
Niki

J'imagine qu'il existe un vtbl créé pour la classe abstraite pour une raison interne (il peut être nécessaire pour une sorte d'informations de type à l'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 qui ne peut pas arriver est.

Pure spéculation

edit: on dirait que je me trompe dans le cas en question. OTOH IIRC certaines langues permettent des appels vtbl à partir du constructeur destructor.

0
BCS