Miembros virtuales

Una función miembro virtual tiene la característica de poder ser sobrecargada por subclases para agregar un comportamiento propio. Pero existen dos lugares en donde las funciones virtuales toman un comportamiento especial: en los constructores y los destructores.

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:

  1. Alojamos un cacho de memoria suficiente como para que entre el universo.
    Algo así como hacer un: this = malloc(sizeof(Universo)) en C.
  2. Construimos el Lugar usando la memoria recién obtenida como puntero this.
  3. Luego construimos el Universo.
  4. 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).


 30 August 2010 | cpp programacion