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