Hasta ahora se han visto los mecanismos de los iteradores, pero entender el por qué son tan importantes necesita un ejemplo más complejo.
Es normal ver el polimorfismo, la creación dinámica de objetos, y los contenedores en un programa orientado a objetos real. Los contendores y la creación dinámica de objetos resuelven el problema de no saber cuantos o que tipo de objetos se necesitarán. Y si el contenedor está configurado para manejar punteros a la clase base, cada vez que se ponga un puntero a una clase derivada hay un upcast (con los beneficios que conlleva de claridad de código y extensibilidad). Como código del final del Volumen 1, este ejemplo reune varios aspectos de todo lo que se ha aprendido - si es capaz de seguir este ejemplo, entonces está preparado para el Volumen 2.
Suponga que esta creando un programa que permite al usuario
editar y producir diferentes clases de dibujos. Cada dibujo es
un objeto que contiene una colección de objetos
Shape
:
//: C16:Shape.h #ifndef SHAPE_H #define SHAPE_H #include <iostream> #include <string> class Shape { public: virtual void draw() = 0; virtual void erase() = 0; virtual ~Shape() {} }; class Circle : public Shape { public: Circle() {} ~Circle() { std::cout << "Circle::~Circle\n"; } void draw() { std::cout << "Circle::draw\n";} void erase() { std::cout << "Circle::erase\n";} }; class Square : public Shape { public: Square() {} ~Square() { std::cout << "Square::~Square\n"; } void draw() { std::cout << "Square::draw\n";} void erase() { std::cout << "Square::erase\n";} }; class Line : public Shape { public: Line() {} ~Line() { std::cout << "Line::~Line\n"; } void draw() { std::cout << "Line::draw\n";} void erase() { std::cout << "Line::erase\n";} }; #endif // SHAPE_H ///:~
Listado 16.29. C16/Shape.h
Se usa la estructura clásica de las funciones virtuales en la
clase base que son sobreescritas en la clase derivada. Hay que
resaltar que la clase Shape
incluye un
destructor virtual, algo que se debería añadir automáticamente a
cualquier clase con funciones virtual
es. Si un contenedor maneja
punteros o referencias a objetos Shape
,
entonces cuando los destructores virtuales sean llamados para
estos objetos todo será correctamente limpiado.
Cada tipo diferente de dibujo en el siguiente ejemplo hace uso
de una plantilla de clase contenedora diferente: el
PStash
y el Stack
que han sido definido en este capítulo, y la clase
vector
de la Librería Estándar de
C++. El «uso» de los contenedores es extremadamente
simple, y en general la herencia no es la mejor aproximación
(composición puede tener más sentido), pero en este caso la
herencia es una aproximación más simple.
//: C16:Drawing.cpp #include <vector> // Uses Standard vector too! #include "TPStash2.h" #include "TStack2.h" #include "Shape.h" using namespace std; // A Drawing is primarily a container of Shapes: class Drawing : public PStash<Shape> { public: ~Drawing() { cout << "~Drawing" << endl; } }; // A Plan is a different container of Shapes: class Plan : public Stack<Shape> { public: ~Plan() { cout << "~Plan" << endl; } }; // A Schematic is a different container of Shapes: class Schematic : public vector<Shape*> { public: ~Schematic() { cout << "~Schematic" << endl; } }; // A function template: template<class Iter> void drawAll(Iter start, Iter end) { while(start != end) { (*start)->draw(); start++; } } int main() { // Each type of container has // a different interface: Drawing d; d.add(new Circle); d.add(new Square); d.add(new Line); Plan p; p.push(new Line); p.push(new Square); p.push(new Circle); Schematic s; s.push_back(new Square); s.push_back(new Circle); s.push_back(new Line); Shape* sarray[] = { new Circle, new Square, new Line }; // The iterators and the template function // allow them to be treated generically: cout << "Drawing d:" << endl; drawAll(d.begin(), d.end()); cout << "Plan p:" << endl; drawAll(p.begin(), p.end()); cout << "Schematic s:" << endl; drawAll(s.begin(), s.end()); cout << "Array sarray:" << endl; // Even works with array pointers: drawAll(sarray, sarray + sizeof(sarray)/sizeof(*sarray)); cout << "End of main" << endl; } ///:~
Listado 16.30. C16/Drawing.cpp
Los distintos tipos de contenedores manejan punteros a
Shape
y punteros a objetos de clases
derivadas de Shape
. Sin embargo, debido
al polimorfismo, cuando se llama a las funcione virtuales ocurre
el comportamiento adecuado.
Note que sarray
, el array de
Shape*
, puede ser recorrido como un
contenedor.
En drawAll()
se ve algo nuevo. En este
capítulo, únicamente hemos estado usando plantillas
de clases, las cuales pueden instanciar nuevas
clases basadas en uno o más parámetros de tipo. Sin embargo,
se puede crear plantillas de función, las
cuales crean nuevas funciones basadas en parámetros de
tipo. La razón para crear una plantilla de función es la misma
por la cual se crea una plantilla de clase: intentar crear
código más genérico, y se hace retrasando la especificación de
uno o más tipos. Se quiere decir que estos parámetros de
tipos soportan ciertas operaciones, no qué tipos exactos son.
Se puede pensar sobre la plantilla función drawAll()
como si fuera un algoritmo
(y así es como se llaman la mayoría de las plantillas de
función de la STL). Sólo dice como hacer algo dado unos
iteradores que describen un rango de elementos, mientras que
estos iteradores pueden ser desreferenciados, incrementados, y
comparados. Estos son exactamente la clase de iteradores que
hemos estado desarrollando en este capítulo, y también - y no
por casualidad - la clase de iteradores que son producidos por
los contenedores de la Librería Estándar de C++, evidenciado
por el uso de vector
en este ejemplo.
Además nos gustaría que drawAll()
fuera un
algoritmo genérico, para que los
contenedores pudieran ser de cualquier tipo y que no se
tuviera que escribir una nueva versión del algoritmo para cada
tipo diferente del contenedor. Aquí es donde las plantillas de
funciones son esenciales, porque automáticamente generan el
código específico para cada tipo de contenedor diferente. Pero
sin la indirección extra proporcionada por los iteradores,
estas generalizaciones no serían posibles. Este es el motivo
por el que los iteradores son importantes; nos permiten
escribir código de propósito general que involucra a
contenedores sin conocer la estructura subyacente del
contenedor. (Note que los iteradores de C++ y los algoritmos
genéricos requieren plantillas de funciones).
Se puede ver el alcance de esto en el
main()
, ya que drawAll()
funciona sin cambiar cada uno de los diferentes
tipos de contenedores. E incluso más interesante,
drawAll()
también funciona con punteros
al principio y al final del array
sarray
. Esta habilidad para tratar arrays
como contenedores está integrada en el diseño de la Librería
Estándar de C++, cuyos algoritmos se parecen mucho a
drawAll()
.
Debido a que las plantillas de clases contenedoras están raramente sujetas a la herencia y al upcast se ven como clases «ordinarias», casi nunca se verán funciones virtuales en clases contenedoras. La reutilización de las clases contenedoras está implementado mediante plantillas, no mediante herencia.