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).