virtuales puro protegidos polimorfismo modificador metodo herencia constructores atributos c++ constructor override virtual-method

protegidos - polimorfismo puro c++



Llamar a funciones virtuales dentro de constructores. (13)

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 establece en 2.

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


¿Sabes el error de bloqueo desde el 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 existe una implementación para la función pureVitualFunction () y la función se llama en el constructor, el programa se bloqueará.


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 aún no existe, por lo que una función virtual no puede funcionar.

Sin embargo, esto se puede resolver con captadores polimórficos que usan polimorfismo estático en lugar de funciones virtuales si los captadores 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 del polimorfismo estático, la clase base sabe a qué clase ''getter'' llamar, ya que la información se proporciona en tiempo de compilación.


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


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

Las funciones de los miembros, incluidas las funciones virtuales (10.3), se pueden llamar durante la construcción o la 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 de los miembros de datos no estáticos de la clase, y el objeto al que se aplica la llamada es el objeto (llámelo x) en construcción o destrucción, la función llamada es el anulador final en la clase del constructor o destructor y no uno que la anule en una clase más derivada. Si la llamada a la función virtual utiliza un acceso explícito a un miembro de la clase (5.2.5) y la expresión del objeto se refiere al objeto completo de x o uno de los subobjetos de la clase base de ese objeto, pero no a x o uno de sus subobjetos de la clase base, el comportamiento no está definido .

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

Por lo tanto, intentar llamar a una función de clase derivada desde una clase base en construcción es peligroso. De manera similar, un objeto se destruye en orden inverso a 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 ya tienen sido liberado.


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.


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


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 en el 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 buen detalle. Sugiero leer eso (y el resto de las preguntas frecuentes) para un seguimiento.

Extracto:

[...] En un constructor, el mecanismo de llamada virtual está deshabilitado porque la anulación de las clases derivadas aún no ha ocurrido. Los objetos se construyen desde la base hacia arriba, "base antes derivada".

[...]

La destrucción se realiza "clase derivada antes de la clase base", por lo que las funciones virtuales se comportan como en los constructores: solo se usan las definiciones locales, y no se realizan llamadas a funciones primordiales para evitar tocar la parte de la clase derivada del objeto (ahora destruida).

EDITAR más corregido a todos (gracias litb)


Llamar a una función polimórfica desde un constructor es una receta para el desastre en la mayoría de los idiomas OO. Diferentes idiomas se desempeñarán de manera diferente cuando se encuentre esta situación.

El problema básico es que en todos los idiomas los tipos base deben 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 (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 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. Una vez que la Base se haya construido completamente, el compilador comenzará a construir la clase Derivada y reemplazará los punteros del método para señalar 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 de base o al constructor derivado. Las implicaciones son diferentes (y para mi gusto más peligrosas). Si el constructor de la clase base llama a un método que está anulado en la clase derivada, la llamada se manejará en el nivel derivado al llamar a un método en un objeto no construido, lo que dará 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 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 puede ver, llamar a un método polimórfico ( virtual en terminología de C ++) es una fuente común de errores. En C ++, al menos tiene la garantía de que nunca llamará a un método en un objeto aún sin construir ...


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 del constructor, el compilador genera código para volver a apuntar el puntero vtable al vtable real para la clase. Esto garantiza que el código del constructor que llama a las funciones virtuales llame a las implementaciones de clase base de esas funciones, no a la anulación en la clase.


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


Otras respuestas ya han explicado por qué virtual llamadas a funciones virtual no funcionan como se esperaba cuando se las llama desde un constructor. En su lugar, me gustaría proponer otro posible trabajo para obtener un comportamiento polimórfico del constructor de un tipo base.

Al agregar un constructor de plantillas al tipo base de modo que siempre se deduzca que el argumento de la plantilla sea el tipo derivado, es posible conocer el tipo concreto del tipo derivado. Desde allí, puede llamar a funciones miembro static para ese tipo derivado.

Esta solución no permite llamar a funciones miembro no static . Mientras que la ejecución está en el constructor del tipo base, el constructor del tipo derivado ni siquiera ha tenido tiempo de revisar su lista de inicialización de miembros. La parte de tipo derivada de la instancia que se está creando no ha comenzado a inicializarse. Y dado que las funciones miembro no static casi seguramente interactúan con miembros de datos, sería inusual querer llamar a las funciones miembro no static del tipo derivado desde el constructor del tipo base.

Aquí hay una implementación de ejemplo:

#include <iostream> #include <string> struct Base { protected: template<class T> explicit Base(const T*) : class_name(T::Name()) { std::cout << class_name << " created/n"; } public: Base() : class_name(Name()) { std::cout << class_name << " created/n"; } virtual ~Base() { std::cout << class_name << " destroyed/n"; } static std::string Name() { return "Base"; } private: std::string class_name; }; struct Derived : public Base { Derived() : Base(this) {} // `this` is used to allow Base::Base<T> to deduce T static std::string Name() { return "Derived"; } }; int main(int argc, const char *argv[]) { Derived{}; // Create and destroy a Derived Base{}; // Create and destroy a Base return 0; }

Este ejemplo debería imprimir

Derived created Derived destroyed Base created Base destroyed

Cuando se construye un Derived , el comportamiento del constructor Base depende del tipo dinámico real del objeto que se está construyendo.


Primero, se crea el Objeto y luego le asignamos su dirección a los punteros. Al momento de la creación del objeto se llama a los Constructores y se utiliza para inicializar el valor de los miembros de los 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 un puntero a un constructor, que puede apuntar a un constructor virtual, porque una de las propiedades de la función virtual es que solo puede ser usada por 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.

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