Para la mayoría de los contenedores tiene sentido tener un iterador. Aquí tenemos un iterador añadido a la clase PStash:
//: C16:TPStash2.h // Templatized PStash with nested iterator #ifndef TPSTASH2_H #define TPSTASH2_H #include "../require.h" #include <cstdlib> template<class T, int incr = 20> class PStash { int quantity; int next; T** storage; void inflate(int increase = incr); public: PStash() : quantity(0), storage(0), next(0) {} ~PStash(); int add(T* element); T* operator[](int index) const; T* remove(int index); int count() const { return next; } // Nested iterator class: class iterator; // Declaration required friend class iterator; // Make it a friend class iterator { // Now define it PStash& ps; int index; public: iterator(PStash& pStash) : ps(pStash), index(0) {} // To create the end sentinel: iterator(PStash& pStash, bool) : ps(pStash), index(ps.next) {} // Copy-constructor: iterator(const iterator& rv) : ps(rv.ps), index(rv.index) {} iterator& operator=(const iterator& rv) { ps = rv.ps; index = rv.index; return *this; } iterator& operator++() { require(++index <= ps.next, "PStash::iterator::operator++ " "moves index out of bounds"); return *this; } iterator& operator++(int) { return operator++(); } iterator& operator--() { require(--index >= 0, "PStash::iterator::operator-- " "moves index out of bounds"); return *this; } iterator& operator--(int) { return operator--(); } // Jump interator forward or backward: iterator& operator+=(int amount) { require(index + amount < ps.next && index + amount >= 0, "PStash::iterator::operator+= " "attempt to index out of bounds"); index += amount; return *this; } iterator& operator-=(int amount) { require(index - amount < ps.next && index - amount >= 0, "PStash::iterator::operator-= " "attempt to index out of bounds"); index -= amount; return *this; } // Create a new iterator that's moved forward iterator operator+(int amount) const { iterator ret(*this); ret += amount; // op+= does bounds check return ret; } T* current() const { return ps.storage[index]; } T* operator*() const { return current(); } T* operator->() const { require(ps.storage[index] != 0, "PStash::iterator::operator->returns 0"); return current(); } // Remove the current element: T* remove(){ return ps.remove(index); } // Comparison tests for end: bool operator==(const iterator& rv) const { return index == rv.index; } bool operator!=(const iterator& rv) const { return index != rv.index; } }; iterator begin() { return iterator(*this); } iterator end() { return iterator(*this, true);} }; // Destruction of contained objects: 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> int PStash<T, incr>::add(T* element) { if(next >= quantity) inflate(); storage[next++] = element; return(next - 1); // Index number } template<class T, int incr> inline 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"); 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: storage[index] = 0; return v; } template<class T, int incr> void PStash<T, incr>::inflate(int increase) { const int tsz = sizeof(T*); T** st = new T*[quantity + increase]; memset(st, 0, (quantity + increase) * tsz); memcpy(st, storage, quantity * tsz); quantity += increase; delete []storage; // Old storage storage = st; // Point to new memory } #endif // TPSTASH2_H ///:~
Listado 16.27. C16/TPStash2.h
La mayor parte de este fichero es una traducción bastante directa a plantilla tanto del anterior PStash como del iterador anidado. Esta vez, sin embargo, los operadores devuelven referencias al iterador actual, que es la aproximación más típica y flexible.
El destructor llama a delete para todos los punteros que contiene, y como la plantilla captura el tipo, se ejecutará la destrucción adecuada. Hay que tener en cuenta que si el contenedor contiene punteros a un tipo de clase base, ese tipo debe tener un destructor virtual para asegurar un limpiado adecuado de los objetos derivados cuyas direcciones hayan sufrido un upcast al colocarlos en el contenedor.
El PStash::iterator sigue el modelo de iterador ligado a un único objeto contenedor durante todo su ciclo de vida. Además, el constructor de copia permite crear un nuevo iterador que apunte a la misma posición que el iterador existente a partir del cual se crea, lo que en la práctica crea un marcador dentro del contenedor. Las funciones miembro operator+= y el operator-= permiten mover un iterador un número de posiciones, mientras se respeten los límites del contenedor. Los operadores sobrecargados de incremento y decremento mueven el iterador una posición. El operator+ produce un nuevo iterador desplazado hacia adelante tantas posiciones como indique el sumando. Como en el ejemplo anterior, los operadores de dereferencia de punteros se usan para operar sobre el elemento al que el iterador está referenciando, y remove() destruye el objeto actual llamando al remove() del contenedor.
Se usa el mismo tipo de código que antes (al estilo de los contenedores de la Librería Estándar de C++) para crear el centinela final: un segundo constructor, la función miembro del contenedor end(), y el operator== y operator!= para comparaciones.
El siguiente ejemplo crea y comprueba dos tipos diferentes de objetos Stash, uno para una nueva clase llamada Int que anuncia su construcción y destrucción y otro que gestiona objetos de la clase string de la librería estándar.
//: C16:TPStash2Test.cpp #include "TPStash2.h" #include "../require.h" #include <iostream> #include <vector> #include <string> using namespace std; class Int { int i; public: Int(int ii = 0) : i(ii) { cout << ">" << i << ' '; } ~Int() { cout << "~" << i << ' '; } operator int() const { return i; } friend ostream& operator<<(ostream& os, const Int& x) { return os << "Int: " << x.i; } friend ostream& operator<<(ostream& os, const Int* x) { return os << "Int: " << x->i; } }; int main() { { // To force destructor call PStash<Int> ints; for(int i = 0; i < 30; i++) ints.add(new Int(i)); cout << endl; PStash<Int>::iterator it = ints.begin(); it += 5; PStash<Int>::iterator it2 = it + 10; for(; it != it2; it++) delete it.remove(); // Default removal cout << endl; for(it = ints.begin();it != ints.end();it++) if(*it) // Remove() causes "holes" cout << *it << endl; } // "ints" destructor called here cout << "\n-------------------\n"; ifstream in("TPStash2Test.cpp"); assure(in, "TPStash2Test.cpp"); // Instantiate for String: PStash<string> strings; string line; while(getline(in, line)) strings.add(new string(line)); PStash<string>::iterator sit = strings.begin(); for(; sit != strings.end(); sit++) cout << **sit << endl; sit = strings.begin(); int n = 26; sit += n; for(; sit != strings.end(); sit++) cout << n++ << ": " << **sit << endl; } ///:~
Listado 16.28. C16/TPStash2Test.cpp
Por conveniencia Int tiene asociado un ostream operator<< para Int& y Int*.
El primer bloque de código en main() está rodeado de llaves para forzar la destrucción de PStash<Int> y, por tanto, el limpiado automático que realiza ese destructor. Se sacan y se borran a mano unos cuantos elementos para mostrar que PStash limpia el resto.
Para ambas instancias de PStash, se crea un iterador y se usa para moverse a través del contenedor. Note la elegancia que produce el uso de estas construcciones; no hay que preocuparse por los detalles de implementación de usar un array. Se le dice al contenedor y al iterador qué hacer y no cómo hacerlo. Esto produce una solución más sencilla de conceptualizar, construir y modificar.