¿De dónde provienen los bloqueos de "llamada de función virtual pura"?

106

A veces noto programas que se bloquean en mi computadora con el error: "llamada de función virtual pura".

¿Cómo se compilan estos programas cuando no se puede crear un objeto a partir de una clase abstracta?

Brian R. Bondy
fuente

Respuestas:

107

Pueden resultar si intenta realizar una llamada de función virtual desde un constructor o destructor. Dado que no puede realizar una llamada de función virtual desde un constructor o destructor (el objeto de clase derivada no se ha construido o ya ha sido destruido), llama a la versión de la clase base, que en el caso de una función virtual pura, no no existe.

(Ver demostración en vivo aquí )

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
fuente
3
¿Alguna razón por la que el compilador no pudo detectar esto, en general?
Thomas
21
En el caso general, no se puede detectar, ya que el flujo del ctor puede ir a cualquier lugar y en cualquier lugar puede llamar a la función virtual pura. Este es el problema de detención 101.
shoosh
9
La respuesta es un poco incorrecta: aún se puede definir una función virtual pura; consulte Wikipedia para obtener más detalles.
Redacción
5
Creo que este ejemplo es demasiado simplista: la doIt()llamada en el constructor se desvirtualiza fácilmente y se envía de Base::doIt()forma estática, lo que solo provoca un error del enlazador. Lo que realmente necesitamos es una situación en la que el tipo dinámico durante un envío dinámico sea ​​el tipo base abstracto.
Kerrek SB
2
Esto se puede activar con MSVC si agrega un nivel adicional de indirección: tiene que Base::Basellamar a un no virtual f()que a su vez llama al doItmétodo virtual (puro) .
Frerich Raabe
64

Además del caso estándar de llamar a una función virtual desde el constructor o destructor de un objeto con funciones virtuales puras, también puede obtener una llamada a función virtual pura (al menos en MSVC) si llama a una función virtual después de que el objeto ha sido destruido . Obviamente, esto es algo bastante malo de intentar y hacer, pero si está trabajando con clases abstractas como interfaces y se equivoca, entonces es algo que puede ver. Es posible que sea más probable si está utilizando interfaces contadas referenciadas y tiene un error de recuento de referencias o si tiene una condición de carrera de uso / destrucción de objetos en un programa de subprocesos múltiples ... Lo que pasa con este tipo de purecall es que es A menudo, es menos fácil comprender lo que está sucediendo, ya que una verificación de los "sospechosos habituales" de las llamadas virtuales en ctor y dtor saldrá limpio.

Para ayudar con la depuración de este tipo de problemas, en varias versiones de MSVC, puede reemplazar el controlador purecall de la biblioteca en tiempo de ejecución. Para ello, proporcione su propia función con esta firma:

int __cdecl _purecall(void)

y vincularlo antes de vincular la biblioteca en tiempo de ejecución. Esto le da a USTED el control de lo que sucede cuando se detecta una llamada pura. Una vez que tenga el control, puede hacer algo más útil que el controlador estándar. Tengo un controlador que puede proporcionar un seguimiento de la pila de dónde ocurrió la llamada pura; consulte aquí: http://www.lenholgate.com/blog/2006/01/purecall.html para obtener más detalles.

(Tenga en cuenta que también puede llamar a _set_purecall_handler () para instalar su controlador en algunas versiones de MSVC).

Len Holgate
fuente
1
Gracias por el puntero sobre obtener una invocación _purecall () en una instancia eliminada; No sabía eso, pero me lo probé a mí mismo con un pequeño código de prueba. Mirando un volcado post mortem en WinDbg, pensé que estaba lidiando con una carrera en la que otro hilo intentaba usar un objeto derivado antes de que se hubiera construido por completo, pero esto arroja una nueva luz sobre el problema y parece ajustarse mejor a la evidencia.
Dave Ruske
1
Una cosa más que agregaré: la _purecall()invocación que normalmente ocurre al llamar a un método de una instancia eliminada no ocurrirá si la clase base ha sido declarada con la __declspec(novtable)optimización (específica de Microsoft). Con eso, es completamente posible llamar a un método virtual anulado después de que se haya eliminado el objeto, lo que podría enmascarar el problema hasta que lo muerda de alguna otra forma. ¡La _purecall()trampa es tu amiga!
Dave Ruske
Es útil conocer a Dave, he visto algunas situaciones recientemente en las que no recibía llamadas puras cuando pensaba que debería. Quizás estaba fallando en esa optimización.
Len Holgate
1
@LenHolgate: Respuesta extremadamente valiosa. Este fue EXACTAMENTE nuestro caso problemático (recuento de referencias incorrecto causado por las condiciones de la carrera). Muchas gracias por señalarnos en la dirección correcta (sospechamos de corrupción de v-table y nos estábamos volviendo locos tratando de encontrar el código culpable)
BlueStrat
7

Por lo general, cuando llama a una función virtual a través de un puntero colgante, lo más probable es que la instancia ya haya sido destruida.

También puede haber razones más "creativas": tal vez haya logrado cortar la parte de su objeto donde se implementó la función virtual. Pero generalmente es solo que la instancia ya ha sido destruida.

Braden
fuente
4

Me encontré con el escenario en el que se llama a las funciones virtuales puras debido a objetos destruidos, Len Holgateya tengo una respuesta muy agradable , me gustaría agregar algo de color con un ejemplo:

  1. Se crea un objeto derivado y el puntero (como clase base) se guarda en algún lugar
  2. El objeto derivado se elimina, pero de alguna manera se sigue haciendo referencia al puntero
  3. Se llama al puntero que apunta al objeto derivado eliminado

El destructor de la clase Derived restablece los puntos vptr a la clase Base vtable, que tiene la función virtual pura, por lo que cuando llamamos a la función virtual, en realidad llama a las virutales puras.

Esto podría suceder debido a un error de código obvio o un escenario complicado de condición de carrera en entornos de subprocesos múltiples.

Aquí hay un ejemplo simple (compilación de g ++ con la optimización desactivada; un programa simple podría optimizarse fácilmente):

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

Y el seguimiento de la pila se ve así:

#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

Realce:

Si el objeto se elimina por completo, lo que significa que se llama al destructor y se recupera Memroy, es posible que simplemente obtengamos un mensaje Segmentation faultporque la memoria ha regresado al sistema operativo y el programa simplemente no puede acceder a él. Por lo tanto, este escenario de "llamada de función virtual pura" generalmente ocurre cuando el objeto se asigna en el grupo de memoria, mientras que se elimina un objeto, la memoria subyacente en realidad no es reclamada por el sistema operativo, todavía está accesible por el proceso.

Baiyan Huang
fuente
0

Supongo que hay un vtbl creado para la clase abstracta por alguna razón interna (podría ser necesario para algún tipo de información de tipo de tiempo de ejecución) y algo sale mal y un objeto real lo obtiene. Es un error. Solo eso debería decir que algo que no puede suceder es.

Pura especulación

editar: parece que estoy equivocado en el caso en cuestión. OTOH IIRC algunos lenguajes permiten llamadas vtbl fuera del constructor destructor.

BCS
fuente
No es un error en el compilador, si eso es lo que quieres decir.
Thomas
Su sospecha es correcta: C # y Java lo permiten. En esos lenguajes, los proyectos en construcción tienen su tipo final. En C ++, los objetos cambian de tipo durante la construcción y por eso y cuándo puede tener objetos con un tipo abstracto.
MSalters
TODAS las clases abstractas y los objetos reales creados a partir de ellos necesitan una vtbl (tabla de funciones virtuales), que enumera las funciones virtuales que se deben llamar en ella. En C ++, un objeto es responsable de crear sus propios miembros, incluida la tabla de funciones virtuales. Los constructores se llaman de la clase base a la derivada y los destructores se llaman de la clase derivada a la base, por lo que en una clase base abstracta la tabla de funciones virtuales aún no está disponible.
fuzzyTew
0

Yo uso VS2010 y cada vez que intento llamar al destructor directamente desde el método público, obtengo un error de "llamada de función virtual pura" durante el tiempo de ejecución.

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

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

Así que moví lo que hay dentro de ~ Foo () para separar el método privado, luego funcionó como un encanto.

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

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

Si usa Borland / CodeGear / Embarcadero / Idera C ++ Builder, puede implementar

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

Durante la depuración, coloque un punto de interrupción en el código y vea la pila de llamadas en el IDE; de lo contrario, registre la pila de llamadas en su controlador de excepciones (o esa función) si tiene las herramientas adecuadas para ello. Yo personalmente uso MadExcept para eso.

PD. La llamada a la función original está en [C ++ Builder] \ source \ cpprtl \ Source \ misc \ pureerr.cpp

Niki
fuente
-2

Aquí hay una forma disimulada de que suceda. Esencialmente me pasó esto hoy.

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 INFORMACIÓN
fuente
1
Al menos no se puede reproducir en mi vc2008, el vptr apunta a la vtable de A cuando se inicializa por primera vez en el constructor de A, pero luego, cuando B está completamente inicializado, el vptr se cambia para apuntar a la vtable de B, lo cual está bien
Baiyan Huang
no podría reproducirlo con vs2010 / 12
makc
I had this essentially happen to me todayobviamente no es cierto, porque simplemente es incorrecto: una función virtual pura se llama solo cuando callFoo()se llama dentro de un constructor (o destructor), porque en este momento el objeto está todavía (o ya) en la etapa A. Aquí hay una versión en ejecución de su código sin el error de sintaxis B b();: los paréntesis la convierten en una declaración de función, desea un objeto.
Wolf