15.10.6. Creación una jerarquía basada en objetos

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