Veamos el siguiente ejemplo:
#include <iostream> using namespace std; class Lugar { public: Lugar() { // El lugar es creado creacion(); // Llamamos a la función virtual } virtual ~Lugar() { // El lugar es destruido destruccion(); // Llamamos a la función virtual } protected: virtual void creacion() { } virtual void destruccion() { } }; class Universo : public Lugar { protected: void creacion() { cout << "Big Bang\n"; } void destruccion() { cout << "Big Crunch\n"; } }; int main() { Universo i_am_god; // ¿Qué imprime este programa? return 0; }
La idea de todo método virtual es poder proporcionar puntos de extensión a las clases derivadas. En este caso, ¿Lugar::creacion funciona como punto de extensión para clases derivadas? La respuesta es: no. El anterior programa no imprime nada.
Aunque creamos una instancia de Universo, ni el método Universo::creacion ni Universo::destruccion se llamaron. ¿A qué se debe esto? Imaginemos que al crear un Universo, primero debemos crear un Lugar en su totalidad, para luego comenzar a crear el universo. Es por eso que si llamamos a creacion mientras estamos construyendo el Lugar, no podemos alcanzar el método Universo::creacion ya que el Universo todavía no comenzó a ser construido (no tiene un Lugar completamente construido donde existir).
La secuencia correcta es:
- Alojamos un cacho de memoria suficiente como para que entre el universo.
Algo así como hacer un: this = malloc(sizeof(Universo)) en C.
- Construimos el Lugar usando la memoria recién obtenida como puntero this.
- Luego construimos el Universo.
- Y recién ahí somos capaces de llamar a Universo::creacion y asegurarnos que estaremos usando el método especializado de la subclase.
¿Puedo llamar a creacion dentro del constructor de Universo? La respuesta es sí, todo es posible. ¿Es correcto? Mmmhh, depende, si alguien hace una subclase de Universo y sobreescribe creacion, nuevamente estará en el mismo problema que estamos mostrando aquí.
¿Por qué en el destructor de Lugar no llama a Universo::destruccion? Porque el Universo ya está destruido (¿cruncheado?) para cuando llegamos al destructor del Lugar.
¿Cómo lo soluciono? Usar puntos de extensión en los constructores y destructores presentan más problemas que ventajas. La solución es no usarlos. Solución (obvia):
#include <iostream> using namespace std; class Lugar { public: Lugar() { } virtual ~Lugar() { } }; class Universo : public Lugar { public: Universo() { cout << "Big Bang\n"; } ~Universo() { cout << "Big Crunch\n"; } }; int main() { Universo i_am_god; return 0; }
En un próximo post voy a dar un ejemplo más complejo donde esto no sirve.
¿Y qué sucede si llamo una función miembro abstracta en el constructor o destructor? Es el desastre total. Completamente ilegal. Imposible. Una violación absoluta a la razón y el sentido común. Imagine este código:
#include <iostream> using namespace std; class Lugar { public: Lugar() { creacion(); } // Ja llamo a creacion() protected: virtual void creacion() = 0; // Jaja y no lo defino ;) }; class Universo : public Lugar { protected: void creacion() { } // Jajaja sólo puede llamarme a mí! }; int main() { Universo i_am_god_x2; return 0; }
Al compilar este código con gcc obtenemos un warning porque estamos llamando un método abstractor en el constructor, y un error de enlace al crear el ejecutable ya que Lugar::creacion no está definido:
test.cpp: In constructor 'Lugar::Lugar()': test.cpp:7:22: warning: abstract virtual 'virtual void Lugar::creacion()' called from constructor C:\temp\ccstr9Hr.o:test.cpp:(.text$_ZN5LugarC2Ev[Lugar::Lugar()]+0x16): undefined reference to `Lugar::creacion()' collect2: ld returned 1 exit status
Aunque dijimos explícitamente que Lugar::creacion() es abstracto (=0), eso no significa que no debamos definirlo en este caso tan particular.
Referencias:
Sutter, Herb & Alexandrescu, Andrei (2004). Item 49: Avoid calling virtual functions in constructors and destructors. C++ Coding Standards: 101 Rules, Guidelines, and Best Practices. Boston, MA: Addison-Wesley Professional, (ISBN 0321113586).