Un asunto que ha aparecido de forma recurrente a lo largo de todo el libro cuando se usaban las clases contenedoras Stack y Stash es el «problema de la propiedad». El «propietario» se refiere a quién o qué es responsable de llamar a delete para aquellos objetos que hayan sido creados dinámicamente (usando new). El problema cuando se usan contenedores es que tienen que ser lo suficientemente flexibles para contener distintos tipos de objetos. Para conseguirlo, los contenedores manejaban punteros a void por lo que no sabían el tipo del objeto que contenían. Borrar un puntero a void no llama al destructor, por lo que el contenedor no podía ser responsable de limpiar sus objetos.
Una solución fue presentada en el ejemplo C14:InheritStack.cpp, en el que Stack era heredado en una nueva clase que aceptaba y producía únicamente punteros a string. Como sabía que sólo podía contener punteros a objetos string, podía borrarlos de manera adecuada. Era una buena solución pero requería heredar una nueva clase contenedora por cada tipo que se quisiera manejar en el contenedor. (Aunque suene un poco tedioso funciona bastante bien como se verá en el Capítulo 16 cuando se introduzcan las plantillas).
El problema es que se quiere que el contenedor maneje más de un tipo, pero no se quieren usar punteros a void. Otra solución es usar polimorfismo forzando a todos los objetos incluidos en el contenedor a ser heredados de la misma clase base. Es decir, el contenedor maneja los objetos de la clase base, y entonces se puede llamar a funciones virtuales - en particular, se puede llamar a destructores virtuales para solucionar el problema de la propiedad.
Esta solución usa lo que se conoce como «jerarquía de raíz única» (singly-rooted hierarchy) o «jerarquía basada en objetos» (object-based hierarchy), siendo el último nombre debido a que la clase raíz de la jerarquía suele llamarse «Object». Resulta que usar una jerarquía de raíz única tiene muchas otras ventajas: de hecho, todos los demás lenguajes orientados a objetos salvo C++ obligan a usar una jerarquía así - cuando se crea una clase se hereda automáticamente de forma directa o indirecta de una clase base común, una clase base que fue establecida por los creadores del lenguaje. En C++, se pensó que forzar a tener una clase base común crearía demasiada sobrecarga, por lo que se desestimó. Sin embargo, se puede elegir usar una clase base común en los propios proyectos, y esta materia será tratada con más detalle en el segundo volumen de este libro.
Para solucionar el problema de la propiedad, se puede crear una clase base Object extremadamente simple, que sólo tiene un destructor virtual. De esta forma Stack puede manejar objetos que hereden de Object:
//: C15:OStack.h // Using a singly-rooted hierarchy #ifndef OSTACK_H #define OSTACK_H class Object { public: virtual ~Object() = 0; }; // Required definition: inline Object::~Object() {} class Stack { struct Link { Object* data; Link* next; Link(Object* dat, Link* nxt) : data(dat), next(nxt) {} }* head; public: Stack() : head(0) {} ~Stack(){ while(head) delete pop(); } void push(Object* dat) { head = new Link(dat, head); } Object* peek() const { return head ? head->data : 0; } Object* pop() { if(head == 0) return 0; Object* result = head->data; Link* oldHead = head; head = head->next; delete oldHead; return result; } }; #endif // OSTACK_H ///:~
Listado 15.16. C15/OStack.h
Para simplificar las cosas manteniendo todo en el fichero de cabecera, la definición (obligatoria) del destructor virtual puro es introducida en línea en el fichero de cabecera, y pop() también está en línea aunque podría ser considerado como demasiado largo para ser incluido así.
Los objetos Link (enlace) ahora manejan punteros a Object en vez de punteros a void, y la Stack (pila) sólo aceptará y devolverá punteros a Object. Ahora Stack es mucho más flexible, ya que puede manejar un montón de tipos diferentes pero además destruirá cualquier objeto que quede en la pila. La nueva limitación (que será finalmente eliminada cuando las plantillas se apliquen al problema en el Capítulo 16) es que todo lo que se ponga en la pila debe ser heredado de Object. Esto está bien si se crea una clase desde la nada, pero ¿qué pasa si ya se tiene una clase como string y se quiere ser capaz de meterla en la pila? En este caso, la nueva clase debe ser al mismo tiempo un string y un Object, lo que significa que debe heredar de ambas clases. Esto se conoce como herencia múltiple y es materia para un capítulo entero en el Volumen 2 de este libro (se puede bajar de www.BruceEckel.com). Cuando se lea ese capítulo, se verá que la herencia múltiple puede generar mucha complejidad, y que es una característica que hay que usar con cuentagotas. Sin embargo, esta situación es lo suficientemente simple como para no caer en ninguna de las trampas de la herencia múltiple:
//: C15:OStackTest.cpp //{T} OStackTest.cpp #include "OStack.h" #include "../require.h" #include <fstream> #include <iostream> #include <string> using namespace std; // Use multiple inheritance. We want // both a string and an Object: class MyString: public string, public Object { public: ~MyString() { cout << "deleting string: " << *this << endl; } MyString(string s) : string(s) {} }; int main(int argc, char* argv[]) { requireArgs(argc, 1); // File name is argument ifstream in(argv[1]); assure(in, argv[1]); Stack textlines; string line; // Read file and store lines in the stack: while(getline(in, line)) textlines.push(new MyString(line)); // Pop some lines from the stack: MyString* s; for(int i = 0; i < 10; i++) { if((s=(MyString*)textlines.pop())==0) break; cout << *s << endl; delete s; } cout << "Letting the destructor do the rest:" << endl; } ///:~
Listado 15.17. C15/OStackTest.cpp
Aunque es similar a la versión anterior del programa de pruebas de Stack, se puede ver que sólo se han sacado 10 elementos de la pila, lo que implica que probablemente quede algún elemento. Como la pila sabe que contiene Objects, el destructor puede limpiarlos de forma adecuada, como se puede ver en la salida del programa gracias a que los objetos MyString muestran un mensaje cuando son destruidos.
Crear contenedores que manejen Objects es un enfoque razonable - si se tiene una jerarquía de raíz única (impuesta por el lenguaje o por algún requisito que obligue a que todas las clases hereden de Object). En este caso, está garantizado que todo es un Object y no es muy complicado usar los contenedores. Sin embargo, en C++ no se puede esperar esto de todas las clases, por lo que se está abocado a tropezar con la herencia múltiple si se adopta este enfoque. Se verá en el Capítulo 16 que las plantillas solucionan este problema de una forma más simple y elegante.