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

5 comments:
Primera pregunta ingenua..Por qué usaste esto
Universo i_am_god;
En lugar de esto
Universo *i_am_god=new Universo();
Evidentemente hay diferencias, acaso espera con new que lo destruya con delete? y en el otro caso?
Más preguntas del mismo jaez,
por qué es protected el constructor? digo, main no está en la misma clase, ni en una derivada, ni amiga pero parece que puede construir igual.. (no vale responder porque la variable se llama i_am_god)
Llamar a la función miembro abstracta desde el constructor me suena a intento de callback (estas bellezas de la programación que quien sabe por qué no son la implementación común de muchas tareas, entiendo que la recursividad sea media peligrosa y quede excluida para casos límites pero los callbacks! deberían usarse más que el do while =P)
Aunque distrae de la temática de función miembro virtual, interesante el tema de la creación, creo que el lugar y el universo son lo mismo, así que no se podría hacer lugar sin estar ya haciendo universo ;)
> Por qué usaste esto
> Universo i_am_god;
> En lugar de esto
> Universo *i_am_god=new Universo();
A fines del ejemplo es más sencillo. La diferencia es que en el primer caso la instancia va a parar al stack, en el segundo va al heap. C++ es uno de los pocos lenguajes que te permite creas instancias de objetos en el stack (el destructor del objeto se llama "automáticamente" al salir del ámbito de la variable).
Fijate que en el caso de usar "i=new Universo()" tenés que hacer un "delete i" explícitamente.
> Evidentemente hay diferencias, acaso espera
> con new que lo destruya con delete? y en el
> otro caso?
Exactamente. Un new va de la mano de un delete, lo mismo que un new[] va de la mano con un delete[]. En el otro caso, es una instancia en el stack (digamos que el puntero "this" dentro del constructor Lugar() se encuentra en el stack). Esto significa que las variables/objetos en el stack no requieren de llamar new/delete porque la memoria la maneja el mismo compilador manipulando la pila (y sí, como te imaginarás, un objeto en la pila es mucho más rápido de alojar que los del heap, aunque tenemos menos tamaño disponible).
> por qué es protected el constructor?
> digo, main no está en la misma clase,
> ni en una derivada, ni amiga
> pero parece que puede construir igual..
"main no está en la misma clase" me sonó a Java :)
La respuesta es que en el segundo pedazo de código me equivoque :P Como bien sabes los constructores deben ser públicos si pueden ser creados por funciones fuera de la clase (como en este caso main()). Ahora lo corrijo :)
> (no vale responder porque la
> variable se llama i_am_god)
Jaja hubiera sido una buena escusa.
> Llamar a la función miembro abstracta
> desde el constructor me suena a intento
> de callback (estas bellezas de la
> programación que quien sabe por qué
> no son la implementación común de muchas
> tareas, entiendo que la recursividad sea
> media peligrosa y quede excluida para
> casos límites pero los callbacks!
> deberían usarse más que el do while =P)
Estoy de acuerdo jaja (continúa abajo...)
> Aunque distrae de la temática de
> función miembro virtual,
Sí. Las funciones virtuales podrían verse como un reemplazo de los peligrosos (pero portables) callbacks de C. En C si no le aplicás el cast adecuado al puntero del callback (diríamos, si no usas exactamente la misma signatura), vas muerto, inclusive si difieren el tipo de llamada (ver http://en.wikipedia.org/wiki/X86_calling_conventions).
Otra solución más parecida a los "callbacks" pero más orientada a objetos, es hacer interfaces que puedan ser "inyectadas" (ver http://es.wikipedia.org/wiki/Inyecci%C3%B3n_de_dependencias):
Ejemplo:
/* Esta clase es como una interfaz de Java */
class Delegate {
public:
virtual ~Delegate() { }
virtual void destruccion(Lugar* lugar) = 0;
};
class Lugar {
public:
Lugar(Delegate* d) : delegate_(d) { }
~Lugar() {
if (delegate_)
delegate_->destruccion(this);
}
private:
Delegate* delegate_;
};
> interesante el tema de la creación,
> creo que el lugar y el universo
> son lo mismo, así que no se podría
> hacer lugar sin estar ya haciendo
> universo ;)
Jajaja El ejemplo da para varias posibilidades: tal vez la jerarquía podría ser al revés: el Lugar es un Univeso. O tal vez el Universo tiene un conjunto de Lugares, pero a la vez, es un Lugar.
> Jaja hubiera sido una buena escusa.
Terrible... para esto sí que no hay excusas :P
Llamar a funciones virtuales puras es ilegal por lo tanto el compilador debe avisar del despiste, lo realmente "peligroso" es llamar a funciones virtuales (no puras) desde el constructor. Al no haber sido completada la construcción de toda la posible jerarquía, el constructor ejecuta siempre su versión de la función miembro virtual... una fuente potencial de bugs muy bonita si no lo tienes en cuenta.
Salu2,
Javi
> lo realmente "peligroso" es llamar a
> funciones virtuales (no puras) desde
> el constructor.
...Y el destructor. Exactamente de eso trata
el post.
> Al no haber sido completada la construcción
> de toda la posible jerarquía, el constructor
> ejecuta siempre su versión de la función
> miembro virtual... una fuente potencial
> de bugs muy bonita si no lo tienes en cuenta.
Esa es la frase perfecta: "al no haber sido
completada la construcción de toda la
posible jerarquía" pueden aparecer bugs.
Como bien decís, el constructor estaría llamando
"su versión de la función miembro virtual", que
corresponde a la función virtual redefinida
en el nivel más profundo que el constructor
pueda alcanzar a ver en su tiempo de ejecución.
E.j. Si estamos en el constructor de Lugar, son
todos los métodos virtuales en Lugar; si estamos
en el constructor de Universo son todos los
métodos virtuales que estén redefinidos en
Universo, o en caso que no estén redefinidos
se acceden a los de Lugar, etc.)
> Llamar a funciones virtuales puras es ilegal
> por lo tanto el compilador debe avisar del
> despiste,
Exactamente, en el último ejemplo muestro este
caso y el correspondiente warning de compilación
y error de linkeado. Si intentáramos arreglar
el error de linkeado, obtendríamos un error
de compilación, por lo tanto es imposible salir
de esta situación. De esta forma, y como
dijiste, es ilegal llamar una función virtual
pura.
Gracias por los comentarios!
Publicar un comentario en la entrada