16.4. Stack y Stash como Plantillas

Los problemas recurrentes de «propiedad» con las clases contenedoras Stack y Stash (Pila y Cola respectivamente) que han sido usadas varias veces a través del libro, vienen del hecho de que estos contenedores no son capaces de saber exactamente que tipo manejan. Lo más cerca que han estado es en el «contenedor» de objectos Stack que se vio al final del capítulo 15 en OStackTest.cpp.

Si el programador cliente no elimina explícitamente todos los punteros a objeto que están almacenados en el contenedor, entonces el contenedor debería ser capaz de eliminar esos punteros de manera adecuada. Es decir, el contenedor «posee» cualquiera de los objetos que no hayan sido eliminados, y es el responsable de limpiarlos. La dificultad radica en que el limpiado requiere conocer el tipo del objeto, y crear un contenedor genérico no requiere conocer el tipo de ese objeto. Con los templates, sin embargo, podemos escribir código que no conozcan el tipo de objeto, y fácilmente instanciar una nueva versión del contenedor por cada tipo que queramos que contenga. La instancia contenedora individual conoce el tipo de objetos que maneja y puede por tanto llamar al destructor correcto (asumiendo que se haya proporcionado un destructor virtual).

Para la pila es bastante sencillo debido a todas las funciones miembro pueden ser introducidas en línea:

//: C16:TStack.h
// The Stack as a template
#ifndef TSTACK_H
#define TSTACK_H

template<class T>
class Stack {
  struct Link {
    T* data;
    Link* next;
    Link(T* dat, Link* nxt): 
      data(dat), next(nxt) {}
  }* head;
public:
  Stack() : head(0) {}
  ~Stack(){ 
    while(head)
      delete pop();
  }
  void push(T* dat) {
    head = new Link(dat, head);
  }
  T* peek() const {
    return head ? head->data : 0; 
  }
  T* pop(){
    if(head == 0) return 0;
    T* result = head->data;
    Link* oldHead = head;
    head = head->next;
    delete oldHead;
    return result;
  }
};
#endif // TSTACK_H ///:~

Listado 16.9. C16/TStack.h


Si se compara esto al ejemplo de OStack.h al final del capítulo 15, se verá que Stack es virtualmente idéntica, excepto que Object ha sido reemplazado con T. El programa de prueba también es casi idéntico, excepto por la necesidad de múltiple herencia de string y Object (incluso por la necesidad de Object en sí mismo) que ha sido eliminada. Ahora no tenemos una clase MyString para anunciar su destrucción por lo que añadimos una pequeña clase nueva para mostrar como la clase contenedora Stack limpia sus objetos:

//: C16:TStackTest.cpp
//{T} TStackTest.cpp
#include "TStack.h"
#include "../require.h"
#include <fstream>
#include <iostream>
#include <string>
using namespace std;

class X {
public:
  virtual ~X() { cout << "~X " << endl; }
};

int main(int argc, char* argv[]) {
  requireArgs(argc, 1); // File name is argument
  ifstream in(argv[1]);
  assure(in, argv[1]);
  Stack<string> textlines;
  string line;
  // Read file and store lines in the Stack:
  while(getline(in, line))
    textlines.push(new string(line));
  // Pop some lines from the stack:
  string* s;
  for(int i = 0; i < 10; i++) {
    if((s = (string*)textlines.pop())==0) break;
    cout << *s << endl;
    delete s; 
  } // The destructor deletes the other strings.
  // Show that correct destruction happens:
  Stack<X> xx;
  for(int j = 0; j < 10; j++)
    xx.push(new X);
} ///:~

Listado 16.10. C16/TStackTest.cpp


El destructor de X es virtual, no porque se sea necesario aquí, sino porque xx podría ser usado más tarde para manejar objetos derivados de X.

Note lo fácil que es crear diferentes clases de Stacks para string y para X. Debido a la plantilla, se consigue lo mejor de los dos mundos: la facilidad de uso de la Stack junto con un limpiado correcto.

16.4.1. Cola de punteros mediante plantillas

Reorganizar el código de PStash en un template no es tan simple porque hay un número de funciones miembro que no deben estar en línea. Sin embargo, como buena plantilla aquellas definiciones de función deben permanecer en el archivo cabecera (el compilador y el enlazador se preocuparán por los problemas de múltiples definiciones). El código parece bastante similar al PStash ordinario excepto que el tamaño del incremento (usado por inflate()) ha sido puesto en el template como un parámetro no de clase con un valor por defecto, para que el tamaño de incremento pueda ser modificado en el momento de la instanciación (esto significa que el tamaño es fijo aunque se podría argumentar que el tamaño de incremento debería ser cambiable a lo largo de la vida del objeto):

//: C16:TPStash.h
#ifndef TPSTASH_H
#define TPSTASH_H

template<class T, int incr = 10>
class PStash {
  int quantity; // Number of storage spaces
  int next; // Next empty space
  T** storage;
  void inflate(int increase = incr);
public:
  PStash() : quantity(0), next(0), storage(0) {}
  ~PStash();
  int add(T* element);
  T* operator[](int index) const; // Fetch
  // Remove the reference from this PStash:
  T* remove(int index);
  // Number of elements in Stash:
  int count() const { return next; }
};

template<class T, int incr>
int PStash<T, incr>::add(T* element) {
  if(next >= quantity)
    inflate(incr);
  storage[next++] = element;
  return(next - 1); // Index number
}

// Ownership of remaining pointers:
template<class T, int incr>
PStash<T, incr>::~PStash() {
  for(int i = 0; i < next; i++) {
    delete storage[i]; // Null pointers OK
    storage[i] = 0; // Just to be safe
  }
  delete []storage;
}

template<class T, int incr>
T* PStash<T, incr>::operator[](int index) const {
  require(index >= 0,
    "PStash::operator[] index negative");
  if(index >= next)
    return 0; // To indicate the end
  require(storage[index] != 0, 
    "PStash::operator[] returned null pointer");
  // Produce pointer to desired element:
  return storage[index];
}

template<class T, int incr>
T* PStash<T, incr>::remove(int index) {
  // operator[] performs validity checks:
  T* v = operator[](index);
  // "Remove" the pointer:
  if(v != 0) storage[index] = 0;
  return v;
}

template<class T, int incr>
void PStash<T, incr>::inflate(int increase) {
  const int psz = sizeof(T*);
  T** st = new T*[quantity + increase];
  memset(st, 0, (quantity + increase) * psz);
  memcpy(st, storage, quantity * psz);
  quantity += increase;
  delete []storage; // Old storage
  storage = st; // Point to new memory
}
#endif // TPSTASH_H ///:~

Listado 16.11. C16/TPStash.h


El tamaño del incremento por defecto es muy pequeño para garantizar que se produzca la llamada a inflate(). Esto nos asegura que funcione correctamente.

Para comprobar el control de propiedad de PStack en template, la siguiente clase muestra informes de creación y destrucción de elementos, y también garantiza que todos los objetos que hayan sido creados sean destruidos. AutoCounter permitirá crear objetos en la pila sólo a los objetos de su tipo:

//: C16:AutoCounter.h
#ifndef AUTOCOUNTER_H
#define AUTOCOUNTER_H
#include "../require.h"
#include <iostream>
#include <set> // Standard C++ Library container
#include <string>

class AutoCounter {
  static int count;
  int id;
  class CleanupCheck {
    std::set<AutoCounter*> trace;
  public:
    void add(AutoCounter* ap) {
      trace.insert(ap);
    }
    void remove(AutoCounter* ap) {
      require(trace.erase(ap) == 1,
        "Attempt to delete AutoCounter twice");
    }
    ~CleanupCheck() {
      std::cout << "~CleanupCheck()"<< std::endl;
      require(trace.size() == 0,
       "All AutoCounter objects not cleaned up");
    }
  };
  static CleanupCheck verifier;
  AutoCounter() : id(count++) {
    verifier.add(this); // Register itself
    std::cout << "created[" << id << "]" 
              << std::endl;
  }
  // Prevent assignment and copy-construction:
  AutoCounter(const AutoCounter&);
  void operator=(const AutoCounter&);
public:
  // You can only create objects with this:
  static AutoCounter* create() { 
    return new AutoCounter();
  }
  ~AutoCounter() {
    std::cout << "destroying[" << id 
              << "]" << std::endl;
    verifier.remove(this);
  }
  // Print both objects and pointers:
  friend std::ostream& operator<<(
    std::ostream& os, const AutoCounter& ac){
    return os << "AutoCounter " << ac.id;
  }
  friend std::ostream& operator<<(
    std::ostream& os, const AutoCounter* ac){
    return os << "AutoCounter " << ac->id;
  }
}; 
#endif // AUTOCOUNTER_H ///:~

Listado 16.12. C16/AutoCounter.h


La clase AutoCounter hace dos cosas. Primero, numera cada instancia de AutoCounter de forma secuencial: el valor de este número se guarda en id, y el número se genera usando el dato miembro count que es static.

Segundo, y más complejo, una instancia estática (llamada verifier) de la clase CleanupCheck se mantiene al tanto de todos los objetos AutoCounter que son creados y destruidos, y nos informa si no se han limpiado todos (por ejemplo si existe un agujero en memoria). Este comportamiento se completa con el uso de la clase set de la Librería Estándar de C++, lo cual es un magnífico ejemplo de cómo las plantillas bien diseñadas nos pueden hacer la vida más fácil (se podrá aprender más de los contenedores en el Volumen 2 de este libro).

La clase set está instanciada para el tipo que maneja; aquí hay una instancia que maneja punteros a AutoCounter. Un set permite que se inserte sólo una instancia de cada objeto; en add() se puede ver que esto sucede con la función set::insert(). insert() nos informa con su valor de retorno si se está intentando añadir algo que ya se había incluido; sin embargo, desde el momento en que las direcciones a objetos se inserten podemos confiar en C++ para que garantice que todos los objetos tengan direcciones únicas.

En remove(), se usa set::erase() para eliminar un puntero a AutoCounter del set. El valor de retorno indica cuantas instancias del elemento se han eliminado; en nuestro caso el valor puede ser únicamente uno o cero. Si el valor es cero, sin embargo, significa que el objeto ya había sido borrado del conjunto y que se está intentando borrar por segunda vez, lo cual es un error de programación que debe ser mostrado mediante require().

El destructor de CleanupCheck hace una comprobación final asegurándose de que el tamaño del set es cero - Lo que significa que todos los objetos han sido eliminados de manera adecuada. Si no es cero, se tiene un agujero de memoria, lo cual se muestra mediante el require().

El constructor y el destructor de AutoCounter se registra y desregistra con el objeto verifier. Hay que resaltar que el constructor, el constructor de copia, y el operador de asignación son private, por lo que la única forma de crear un objeto es con la función miembro static create() - esto es un ejemplo sencillo de una factory, y garantiza que todos los objetos sean creados en el montón (heap), por lo que verifier no se verá confundido con sobreasignaciones y construcciones de copia.

Como todas las funciones miembro han sido definidas inline, la única razón para el archivo de implementación es que contenga las definiciones de los datos miembro:

//: C16:AutoCounter.cpp {O}
// Definition of static class members
#include "AutoCounter.h"
AutoCounter::CleanupCheck AutoCounter::verifier;
int AutoCounter::count = 0;
///:~

Listado 16.13. C16/AutoCounter.cpp


Con el AutoCounter en la mano, podemos comprobar las facilidades que proporciona el PStash. El siguiente ejemplo no sólo muestra que el destructor de PStash limpia todos los objetos que posee, sino que también muestra como la clase AutoCounter detecta a los objetos que no se han limpiado.

//: C16:TPStashTest.cpp
//{L} AutoCounter
#include "AutoCounter.h"
#include "TPStash.h"
#include <iostream>
#include <fstream>
using namespace std;

int main() {
  PStash<AutoCounter> acStash;
  for(int i = 0; i < 10; i++)
    acStash.add(AutoCounter::create());
  cout << "Removing 5 manually:" << endl;
  for(int j = 0; j < 5; j++)
    delete acStash.remove(j);
  cout << "Remove two without deleting them:"
       << endl;
  // ... to generate the cleanup error message.
  cout << acStash.remove(5) << endl;
  cout << acStash.remove(6) << endl;
  cout << "The destructor cleans up the rest:"
       << endl;
  // Repeat the test from earlier chapters: 
  ifstream in("TPStashTest.cpp");
  assure(in, "TPStashTest.cpp");
  PStash<string> stringStash;
  string line;
  while(getline(in, line))
    stringStash.add(new string(line));
  // Print out the strings:
  for(int u = 0; stringStash[u]; u++)
    cout << "stringStash[" << u << "] = "
         << *stringStash[u] << endl;
} ///:~

Listado 16.14. C16/TPStashTest.cpp


Cuando se eliminan los elementos AutoCounter 5 y 6 de la PStash, se vuelve responsabilidad del que los llama, pero como el cliente nunca los borra se podrín producir agujeros de memoria, que serín detectados por AutoCounter en tiempo de ejecución.

Cuando se ejecuta el programa, se verá que el mensaje de error no es tan específico como podría ser. Si se usa el esquema presentado en AutoCounter para descubrir agujeros de memoria en nuestro sistema, probablemente se quiera imprimir información más detallada sobre los objetos que no se hayan limpiado. El Volumen 2 de este libro muestra algunas formas más sofisticadas de hacer esto.