Pregunta Llamar funciones virtuales dentro de constructores


Supongamos que tengo dos clases de C ++:

class A
{
public:
  A() { fn(); }

  virtual void fn() { _n = 1; }
  int getn() { return _n; }

protected:
  int _n;
};

class B : public A
{
public:
  B() : A() {}

  virtual void fn() { _n = 2; }
};

Si escribo el siguiente código:

int main()
{
  B b;
  int n = b.getn();
}

Uno podría esperar que n está establecido en 2.

Resulta que n se establece en 1. ¿Por qué?


184
2018-06-07 15:46


origen


Respuestas:


Llamar a las funciones virtuales desde un constructor o destructor es peligroso y debe evitarse siempre que sea posible. Todas las implementaciones de C ++ deben llamar a la versión de la función definida en el nivel de la jerarquía en el constructor actual y no más allá.

los Preguntas frecuentes sobre C ++ Lite cubre esto en la sección 23.7 con bastante buen detalle. Sugiero leer eso (y el resto de las preguntas frecuentes) para un seguimiento.

EDITAR Corregido más a todos (gracias litb)


175
2018-06-07 15:52



Llamar a una función polimórfica de un constructor es una receta para el desastre en la mayoría de los lenguajes OO. Los diferentes idiomas funcionarán de manera diferente cuando se encuentre esta situación.

El problema básico es que, en todos los idiomas, los tipos de Base deben construirse antes del tipo Derivado. Ahora, el problema es qué significa llamar a un método polimórfico del constructor. ¿Cómo esperas que se comporte? Hay dos enfoques: llamar al método en el nivel Base (estilo C ++) o llamar al método polimórfico en un objeto no estructurado en la parte inferior de la jerarquía (forma Java).

En C ++, la clase Base construirá su versión de la tabla de métodos virtuales antes de ingresar a su propia construcción. En este punto, una llamada al método virtual terminará llamando a la versión Base del método o produciendo un método virtual puro llamado en caso de que no tenga implementación en ese nivel de la jerarquía. Después de que la Base se haya construido por completo, el compilador comenzará a construir la clase Derivada, y anulará los punteros del método para apuntar a las implementaciones en el siguiente nivel de la jerarquía.

class Base {
public:
   Base() { f(); }
   virtual void f() { std::cout << "Base" << std::endl; } 
};
class Derived : public Base
{
public:
   Derived() : Base() {}
   virtual void f() { std::cout << "Derived" << std::endl; }
};
int main() {
   Derived d;
}
// outputs: "Base" as the vtable still points to Base::f() when Base::Base() is run

En Java, el compilador creará la tabla virtual equivalente en el primer paso de la construcción, antes de ingresar al constructor Base o al constructor Derivado. Las implicaciones son diferentes (y para mis aficiones más peligrosas). Si el constructor de la clase base llama a un método que se reemplaza en la clase derivada, la llamada se manejará realmente en el nivel derivado llamando a un método en un objeto no reconstruido, produciendo resultados inesperados. Todos los atributos de la clase derivada que se inicializan dentro del bloque constructor aún no se han inicializado, incluidos los atributos 'finales'. Los elementos que tienen un valor predeterminado definido en el nivel de clase tendrán ese valor.

public class Base {
   public Base() { polymorphic(); }
   public void polymorphic() { 
      System.out.println( "Base" );
   }
}
public class Derived extends Base
{
   final int x;
   public Derived( int value ) {
      x = value;
      polymorphic();
   }
   public void polymorphic() {
      System.out.println( "Derived: " + x ); 
   }
   public static void main( String args[] ) {
      Derived d = new Derived( 5 );
   }
}
// outputs: Derived 0
//          Derived 5
// ... so much for final attributes never changing :P

Como ves, llamando a un polimórfico (virtual en la terminología C ++) los métodos son una fuente común de errores. En C ++, al menos tienes la garantía de que nunca llamará a un método en un objeto aún no construido ...


73
2018-06-07 16:56



La razón es que los objetos C ++ se construyen como cebollas, de adentro hacia afuera. Las superclases se construyen antes de las clases derivadas. Entonces, antes de que se pueda hacer una B, se debe hacer una A. Cuando se llama al constructor de A, todavía no es una B, por lo que la tabla de funciones virtuales todavía tiene la entrada para la copia de A de fn ().


49
2018-06-07 15:46



los Preguntas frecuentes sobre C ++ LiteCubre esto bastante bien:

Básicamente, durante la llamada al constructor de las clases base, el objeto aún no es del tipo derivado y, por lo tanto, se llama a la implementación del tipo base de la función virtual y no al tipo derivado.


21
2018-06-07 16:03



Una solución a su problema es usar métodos de fábrica para crear su objeto.

  • Defina una clase base común para su jerarquía de clases que contenga un método virtual afterConstruction ():
Objeto de clase
{
público:
  virtual void afterConstruction () {}
  // ...
};
  • Definir un método de fábrica:
plantilla <clase C>
C * factoryNew ()
{
  C * pObject = new C ();
  pObject-> afterConstruction ();

  return pObject;
}
  • Úselo así:
clase MyClass: public Object
{
público:
  virtual void afterConstruction ()
  {
    // hacer algo.
  }
  // ...
};

MyClass * pMyObject = factoryNew ();


12
2018-06-07 17:09



¿Conoces el error del explorador de Windows? "Llamada de función virtual pura ..."
El mismo problema ...

class AbstractClass 
{
public:
    AbstractClass( ){
        //if you call pureVitualFunction I will crash...
    }
    virtual void pureVitualFunction() = 0;
};

Debido a que no hay implementación para la función pureVitualFunction () y se llama a la función en el constructor, el programa se bloqueará.


1
2018-06-07 16:51



Los vtables son creados por el compilador. Un objeto de clase tiene un puntero a su vtable. Cuando comienza la vida, ese puntero vtable apunta al vtable de la clase base. Al final del código de constructor, el compilador genera código para volver a señalar el puntero vtable al vtable real para la clase. Esto garantiza que el código de constructor que llama a funciones virtuales llama al implementaciones de clase base de esas funciones, no la anulación en la clase.


1
2018-05-07 10:14