Un asunto que ha aparecido de forma recurrente a lo largo de
todo el libro cuando se usaban las clases
Stack
y Stash
es
el "problema de la propiedad". El "propietario" se refiere a
quien o al que sea responsable de llamar al
delete
de aquellos objetos que hayan sido
creados dinámicamente (usando new
). El
problema cuando se usan contenedores es que es necesario ser lo
suficientemente flexible para manejar distintos tipos de
objetos. Para conseguirlo, los contenedores manejan punteros a
void
por lo que no pueden saber el tipo del
objeto que están manejando. Borrar un puntero a
void
no llama al destructor, por lo que el
contenedor no puede ser responsable de borrar 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 objetos
string
, por lo que se les podía borrar de
manera adecuada. Era una buena solución pero requería heredar
una nueva clase contenedera 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 las
plantillas o templates sean introducidos).
El problema es que se quiere que el contenedor maneje más de un
tipo, pero sólo 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 sólo hay que usar funciones
virtuales - en particular, se pueden llamar a destructores
virtuales para solucionar el problema de pertenencia.
Esta solución usa lo que se conoce como "jerarquía de raiz única" (singly-rooted hierarchy) o "jerarquía basada en objetos" (object-based hierarchy), siendo el último nombre debido a que la clase raiz de la jerarquía suele ser llamada "Objeto". Además, el usar jerarquía de raiz única, tiene como resultado otros beneficios: de hecho, cualquier otro lenguaje orientado a objetos que no sea el C++ obliga a usar una jerarquía - 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 penso que forzar a tener una base clase común crearía demasiada sobrecarga, por lo que se desestimó. Sin embargo, se puede elegir usar en nuestros proyectos una clase base común, y esta materia será tratada en el segundo volumen de este libro.
Para solucionar el problema de pertenencia, 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 se crea todo en el fichero cabecera,
la definición (requerida) del destructor virtual puro es
introducida en línea el el fichero cabecera, y
pop()
también está en línea aunque podría
ser considearado como demasiado largo para ser incluido así.
Los objetos Link
(lista) 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 es capaz de
destruirá cualquier objeto dejado 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 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 este capítulo, se verá que la
herencia múltiple genera un montón de complejidad, y que es una
característica que hay que usar con cuentagotas. Sin embargo,
ésta situación es lo suficiéntemente simple como para no tener
problemas al usar 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 ahora maneja
Object
s, 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 Object
s es
una aproximación razonable - si se tiene una jerarquía de raiz
única (debido al lenguaje o por algún requerimiento 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 contenedores. Sin embargo, en C++ no se puede
esperar este comportamiento de todas las clases, por lo que se
está abocado a usar herencia múltiple si se quiere usar esta
aproximación. Se verá en el capítulo 16 que las plantillas
solucionan este problema de una forma más simple y elegante.