Polimorfismo en C++

Hoy descubrí un característica interesante sobre como funciona el polimorfismo en C++. Estaba obteniendo un mensaje de error del compilador (del linker, en realidad) que nunca había recibido antes.

(An english version of this post can be found here)

En mi código disponía de un fragmento similar al que presento a continuación, donde se define una clase abstracta “Base” y la clase “Derived” que hereda de esta mediante herencia pública. En C++ existen tres tipos distintos de herencia: Herencia Pública, Herencia Protegida y Herencia Privada. A los efectos de este artículo, lo que necesitan saber es que la Herencia Pública es similar a la que encontramos en otros lenguajes, como Java, C# u Objective-C.

#include <iostream>
using std::cout;
using std::endl;

class Base
{
	public:
		Base();
		virtual ~Base();
		virtual void printClassName() = 0; // =0 significa "abstract"
};

class Derived : public Base
{
	public:
		Derived();
		virtual ~Derived() { }
		virtual void printClassName();

};

Base::Base()
{
	this->printClassName();
}

Base::~Base()
{
}

Derived::Derived() : Base()
{
}

Derived::~Derived()
{
}

void Derived::printClassName()
{
	cout << "\"Derived\"" << endl;
}

int main()
{
	Derived* d = new Derived();
	delete d;
	return 0;
}

Ávidos conocedores de patrones de diseño probablemente hayan notado que este fragmento es una implementación del patrón Factory Method, donde tenemos una invocación a un método virtual, abstracto en la clase padre, que imprime el nombre de la clase que estamos instanciando.

El problema de este fragmento es que cuando invocamos el compilador, obtenemos el siguiente mensaje de error:

ale@syaoran factory]$ g++ main.cpp -Wall
Undefined symbols:
  "Base::printClassName()", referenced from:
      Base::Base()  in ccNxv98U.o
      Base::Base()  in ccNxv98U.o
ld: symbol(s) not found
collect2: ld returned 1 exit status

Efectivamente, GCC nos advierte que no existe una implementación para el método abstracto “printClassName” y aborta!

¿Por qué sucede esto? El problema radica en la forma en que C++ maneja la herencia. Cuando se instancia un objeto cuya clase deriva de otra clase, primero se invoca el constructor de la clase base y luego se invoca el constructor de la clase derivada. Esto implica que el objeto derivado literalmente no existe hasta que se haya terminado de construir el objeto base “interior”.

En el caso de C++, esto implica que la tabla virtual (utilizada en tiempo de ejecución para aplicar polimorfismo) para el objeto derivado no es creada hasta que la construcción del objeto base se haya creado. Como nosotros estamos invocando un método abstracto dentro del constructor de la clase base, el GCC asume que se invocará la implementación del método en la clase base. Dado que éste no existe (ya que es abstracto), el linker tira un error.

Peor aún, si el método no fuese abstracto (pero sí virtual), el polimorfismo no funcionaría, invocando la implementación en la clase base y no en la clase derivada.

¿Cómo solucionamos este problema?

La solución, afortunadamente, es sencilla. Simplemente debemos tomar como regla general el siguiente enunciado:

“Nunca invocar métodos virtuales sobre objetos parcialmente construidos”.

Para reparar nuestro programa anterior, lo que haremos entonces, es remover la invocación al método virtual del constructor. Dónde ponerlo es una cuestión que, dependiendo de la aplicación, puede ser en un lugar u otro. Una opción que nos permite no cambiar demasiado el diseño es separar el proceso de construcción de un objeto en dos fases: una donde todos los constructores base se ejecutan y una donde se invoque un método especial init() desde el constructor la clase derivada que termine de inicializar el objeto. Esto nos permitirá trabajar una vez que toda la tabla de métodos virtual se encuentre creada.

Nuestro ejemplo reparado se vería de la siguiente forma:

#include <iostream>
using std::cout;
using std::endl;

class Base
{
	public:
		Base();
		virtual ~Base();
		virtual void init();
		virtual void printClassName() = 0; // =0 significa "abstract"
};

class Derived : public Base
{
	public:
		Derived();
		virtual ~Derived();
		virtual void printClassName();
};

Base::Base()
{
	//Invocación movida a init()
}

Base::~Base()
{
}

void Base::init()
{
	printClassName();
}

Derived::Derived() : Base()
{
}

Derived::~Derived()
{
}

void Derived::printClassName()
{
	cout << "\"Derived\"" << endl;
}

int main()
{
	Derived* d = new Derived();
	d->init();
	delete d;
	return 0;
}

Esta versión del código compila y linkea sin problemas. Al ejecutarla, felizmente obtenemos los siguientes resultados:

[ale@syaoran factory]$ ./a.out
"Derived"

Restaría probar como resuelven este problema otros lenguajes de programación orientados a objetos. Lo dejo como ejercicio para el lector 😉

This entry was posted in C++, Programacion. Bookmark the permalink.