Los problemas recurrentes de «propiedad» con las clases contenedoras Stash y Stack que se han ido revisando a lo largo del libro vienen del hecho de que estos contenedores no han sido capaces de saber exactamente qué tipos manejan. Lo más cerca que han estado es el Stack «contenedor de Object» 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 una clase contenedora genérica requiere no conocer el tipo de ese objeto. Con las plantillas, sin embargo, podemos escribir código que no conozca el tipo de objeto, y fácilmente instanciar una nueva versión del contenedor por cada tipo que queramos que contenga. Cada contenedor instanciado conoce el tipo de objetos que maneja y puede por tanto llamar al destructor correcto (suponiendo, en el caso típico en el que interviene el polimorfismo, que se haya proporcionado un destructor virtual).
Para Stack esto resulta bastante sencillo, ya que todas las funciones miembro pueden ser razonablemente inline:
//: 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 con el ejemplo de OStack.h al final del Capítulo 15, se verá que Stack es virtualmente idéntica, excepto que Object ha sido reemplazado por T. El programa de prueba también es casi idéntico, excepto que se ha eliminado la necesidad de herencia múltiple de string y Object (e incluso la necesidad del propio Object ). Ahora no hay una clase MyString que anuncie su destrucción, por lo que se añade una pequeña clase nueva para mostrar cómo un contenedor 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 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. Gracias a la plantilla, se consigue lo mejor de los dos mundos: la facilidad de uso de la clase Stack junto con un limpiado correcto.
Reorganizar el código de PStash en una plantilla no es tan simple porque hay varias funciones miembro que no deberían ser inline. Sin embargo, al tratarse de una plantilla, esas definiciones de función siguen perteneciendo al fichero de cabecera (el compilador y el enlazador se ocupan de los problemas de definición múltiple). El código parece bastante similar al PStash ordinario excepto que el tamaño del incremento (usado por inflate()) ha sido puesto en la plantilla 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 del incremento es fijo; también se podría argumentar que debería poder cambiarse 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 de incremento por defecto que se usa aquí es pequeño para garantizar que se produzcan llamadas a inflate(). Así podemos asegurarnos de que funciona correctamente.
Para comprobar el control de propiedad de PStash en plantilla, la siguiente clase informa de la creación y destrucción de sus propios objetos, y también garantiza que todos los objetos que hayan sido creados sean destruidos. AutoCounter sólo permitirá crear objetos de su tipo en la pila:
//: 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 atributo count que es static.
Segundo, y más complejo, una instancia estática (llamada verifier) de la clase anidada CleanupCheck se mantiene al tanto de todos los objetos AutoCounter que son creados y destruidos, e informa al programador si no se han limpiado todos (es decir, si existe una fuga de memoria). Este comportamiento se consigue usando la clase set de la Librería Estándar de C++, que es un magnífico ejemplo de cómo las plantillas bien diseñadas nos pueden hacer la vida más fácil (en el Volumen 2 de este libro, disponible en línea, se puede aprender todo sobre los contenedores de la Librería Estándar de C++).
La clase set es una plantilla parametrizada con el tipo que contiene; aquí se instancia para manejar punteros a AutoCounter. Un set permite que se inserte sólo una instancia de cada objeto distinto; en add() se puede ver que esto sucede con la función set::insert(). En realidad, insert() informa con su valor de retorno de si se está intentando añadir algo que ya se había incluido; sin embargo, como lo que se añaden son direcciones de objetos, podemos confiar en la garantía de C++ de que todos los objetos tienen direcciones únicas.
En remove(), se usa set::erase() para eliminar un puntero a AutoCounter del set. El valor de retorno indica cuántas instancias del elemento se han eliminado; en nuestro caso sólo se espera cero o uno. 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 se notificará 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 una fuga de memoria, lo cual se muestra mediante el require().
El constructor y el destructor de AutoCounter se registran y se dan de baja en 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ículo, por lo que verifier no se verá confundido por asignaciones y construcciones de copia.
Como todas las funciones miembro han sido definidas inline, la única razón para el fichero de implementación es que contenga las definiciones de los atributos estáticos:
//: 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 en ese momento, sino que también muestra cómo la clase AutoCounter detecta 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 del PStash, pasan a ser responsabilidad de quien hace la llamada, pero como éste nunca los limpia, provocan fugas de memoria, que AutoCounter detecta 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 fugas de memoria en un sistema propio, 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.