Llamar a 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 se establezca en 2.

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

Author: Cheers and hth. - Alf, 2009-06-07

12 answers

Llamar a 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 al nivel de la jerarquía en el constructor actual y no más.

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

EDITAR Corregido más a Todos (gracias litb)

 177
Author: JaredPar,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2015-05-20 18:48:01

Llamar a una función polimórfica desde 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 el tipo(s) Base (es) debe (n) construirse antes del tipo Derivado. Ahora, el problema es qué significa llamar a un método polimórfico desde el constructor. ¿Cómo esperas que se comporte? Hay dos enfoques: llamar al método en el nivel base (C++ estilo) o llamar al método polimórfico en un objeto no construido 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 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 haya sido completamente construida, 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 construirá el equivalente de la tabla virtual en el primer paso de la construcción, antes de ingresar al constructor Base o constructor Derivado. Las implicaciones son diferentes (y a mis gustos más peligrosos). Si el constructor de clase base llama a un método que está sobreescrito en la clase derivada, la llamada en realidad se manejará en el nivel derivado llamando a un método en un objeto no construido, produciendo resultados inesperados. Todos los atributos de la clase derivada que se inicializan dentro del bloque constructor aún no están inicializados, incluidos los atributos 'finales'. Los elementos que tienen un valor predeterminado definido a 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 puede ver, llamar a métodos polimórficos ( virtuales en terminología C++) es una fuente común de errores. En C++, al menos tienes la garantice que nunca llamará a un método en un objeto aún no construido...

 74
Author: David Rodríguez - dribeas,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2009-06-07 17:02:16

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

 50
Author: David Coufal,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2009-06-07 15:46:40

El C++ FAQ Lite Cubre esto bastante bien:

Esencialmente, durante la llamada al constructor de 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
Author: Aaron Maenpaa,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2018-07-09 08:13:20

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 ():
class Object
{
public:
  virtual void afterConstruction() {}
  // ...
};
  • Definir un método de fábrica:
template< class C >
C* factoryNew()
{
  C* pObject = new C();
  pObject->afterConstruction();

  return pObject;
}
  • Úsalo así:
class MyClass : public Object 
{
public:
  virtual void afterConstruction()
  {
    // do something.
  }
  // ...
};

MyClass* pMyObject = factoryNew();

 12
Author: Tobias,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2012-08-15 05:21:09

¿Conoce el error de bloqueo del explorador de Windows?! "Llamada a 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 la función es llamada en el constructor, el programa se bloqueará.

 1
Author: TimW,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2009-06-07 16:51:29

Las vtables son creadas por el compilador. Un objeto de clase tiene un puntero a su vtable. Cuando comienza su vida, ese puntero vtable apunta a la vtable de la clase base. Al final del código del constructor, el compilador genera código para volver a apuntar el puntero vtable a la tabla virtual real de la clase. Esto garantiza que el código del constructor que llama a las funciones virtuales implementaciones de clase base de esas funciones, no la anulación en la clase.

 1
Author: Yogesh,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2013-05-07 10:14:14

El estándar C++ (ISO / IEC 14882-2014) dice:

Las funciones miembro, incluidas las funciones virtuales (10.3), se pueden llamar durante la construcción o destrucción (12.6.2). Cuando una función virtual se llama directa o indirectamente desde un constructor o desde un destructor, incluso durante la construcción o destrucción del los miembros de datos no estáticos de la clase, y el objeto al que se aplica es el objeto (llámelo x) en construcción o destrucción, el función llamada es el overrider final en el constructor o la clase de destructor y no una que la anule en una clase más derivada. Si la llamada a la función virtual utiliza un acceso explícito de miembro de clase (5.2.5) y la expresión objeto se refiere al objeto completo de x o uno de los subobjetos de clase base de ese objeto, pero no x o uno de sus subobjetos de clase base, el comportamiento es undefined.

Por lo tanto, no invoque virtual funciones de constructores o destructores que intenta llamar al objeto en construcción o destrucción, Porque el orden de construcción comienza desde base hasta derivado y el orden de los destructores comienza desde derivado hasta la clase base.

Por lo tanto, intentar llamar a una función de clase derivada de una clase base en construcción es peligroso.Del mismo modo, un objeto se destruye en orden inverso desde la construcción, por lo que intentar llamar a una función en una clase más derivada desde un destructor puede acceder a recursos que tienen ya ha sido liberado.

 1
Author: rsp,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-08-23 07:03:24

En primer lugar,se crea el objeto y luego le asignamos su dirección a los punteros.Los constructores se llaman en el momento de la creación del objeto y se utilizan para inicializar el valor de los miembros de datos. El puntero al objeto entra en el escenario después de la creación del objeto. Por eso, C++ no nos permite hacer constructores como virtuales . .otra razón es que, No hay nada como puntero a constructor, que puede apuntar a constructor virtual, porque una de las propiedades de la función virtual es que puede ser utilizado por solo punteros.

  1. Las funciones virtuales se utilizan para asignar valor dinámicamente,ya que los constructores son estáticos,por lo que no podemos hacerlos virtuales.
 0
Author: Priya,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-11-10 00:48:26

Como se ha señalado, los objetos se crean en base a la construcción. Cuando se está construyendo el objeto base, el objeto derivado no existe todavía, por lo que una función virtual no puede funcionar.

Sin embargo, esto se puede resolver con getters polimórficos que usan polimorfismo estático en lugar de funciones virtuales si sus getters devuelven constantes, o de lo contrario se pueden expresar en una función miembro estática, Este ejemplo usa CRTP ( https://en.wikipedia.org/wiki/Curiously_recurring_template_pattern).

template<typename DerivedClass>
class Base
{
public:
    inline Base() :
    foo(DerivedClass::getFoo())
    {}

    inline int fooSq() {
        return foo * foo;
    }

    const int foo;
};

class A : public Base<A>
{
public:
    inline static int getFoo() { return 1; }
};

class B : public Base<B>
{
public:
    inline static int getFoo() { return 2; }
};

class C : public Base<C>
{
public:
    inline static int getFoo() { return 3; }
};

int main()
{
    A a;
    B b;
    C c;

    std::cout << a.fooSq() << ", " << b.fooSq() << ", " << c.fooSq() << std::endl;

    return 0;
}

Con el uso de polimorfismo estático, la clase base sabe a qué clase' getter llamar ya que la información se proporciona en tiempo de compilación.

 0
Author: stands2reason,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2018-04-11 03:30:42

No veo la importancia de la palabra clave virtual aquí. b es una variable de tipo estático, y su tipo es determinado por el compilador en tiempo de compilación. Las llamadas a la función no hacen referencia a la vtable. Cuando se construye b, se llama al constructor de su clase padre, por lo que el valor de _n se establece en 1.

 -3
Author: user2305329,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2013-10-28 06:58:10

Durante la llamada del constructor del objeto, la tabla del puntero de la función virtual no se construye completamente. Hacer esto generalmente no le dará el comportamiento que espera. Llamar a una función virtual en esta situación puede funcionar, pero no está garantizado y debe evitarse para ser portátil y seguir el estándar de C++.

 -4
Author: terry,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2009-06-07 17:00:14