16.8. Por qué usar iteradores

Hasta ahora se han visto los mecanismos de los iteradores, pero para entender por qué son tan importantes hace falta un ejemplo más complejo.

Es normal ver el polimorfismo, la creación dinámica de objetos y los contenedores usados conjuntamente en un programa orientado a objetos real. Los contenedores y la creación dinámica de objetos resuelven el problema de no saber cuántos objetos ni de qué tipo se necesitarán. Y si el contenedor está configurado para manejar punteros a objetos de la clase base, cada vez que se ponga en el contenedor un puntero a una clase derivada se produce un upcast (con los beneficios que conlleva de organización del código y extensibilidad). Como último código del Volumen 1 de este libro, este ejemplo reúne también varios aspectos de todo lo que se ha aprendido hasta ahora; si es capaz de seguir este ejemplo, entonces está preparado para el Volumen 2.

Suponga que está creando un programa que permite al usuario editar y producir diferentes tipos 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 redefinidas 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 virtuales. Si un contenedor maneja punteros o referencias a objetos Shape, entonces cuando se llame a los destructores virtuales de esos objetos todo se limpiará correctamente.

Cada tipo diferente de dibujo en el siguiente ejemplo hace uso de una plantilla de clase contenedora diferente: el PStash y el Stack que se han 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 puede que la herencia no sea la mejor aproximación (la composición podría tener más sentido), pero en este caso la herencia es una aproximación sencilla y no resta valor a lo que el ejemplo quiere mostrar.

//: 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 (convertidos mediante upcast) de clases derivadas de Shape. Sin embargo, debido al polimorfismo, cuando se llama a las funciones virtuales se sigue produciendo el comportamiento adecuado.

Note que sarray, el array de Shape*, también puede considerarse un contenedor.

16.8.1. Plantillas de función

En drawAll() se ve algo nuevo. Hasta ahora, en este capítulo únicamente hemos estado usando plantillas de clase, que instancian nuevas clases basadas en uno o más parámetros de tipo. Sin embargo, con la misma facilidad se pueden crear plantillas de función, que 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: se intenta crear código más genérico, y se hace retrasando la especificación de uno o más tipos. Sólo se quiere decir que esos parámetros de tipo admiten ciertas operaciones, no qué tipos exactos son.

Se puede pensar en la plantilla de función drawAll() como en un algoritmo (que es como se llama a la mayoría de las plantillas de función de la Librería Estándar de C++). Sólo dice cómo hacer algo dados unos iteradores que describen un rango de elementos, siempre que esos iteradores se puedan dereferenciar, incrementar y comparar. Ésos son exactamente el tipo de iteradores que hemos estado desarrollando en este capítulo, y también (no por casualidad) el tipo de iteradores que producen los contenedores de la Librería Estándar de C++, como demuestra 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 de contenedor. Aquí es donde las plantillas de función 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, esta genericidad no sería posible. Éste 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, en C++, los iteradores y los algoritmos genéricos necesitan plantillas de función para funcionar).

Se puede ver la prueba de esto en el main(), ya que drawAll() funciona sin cambios con cada uno de los diferentes tipos de contenedor. 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 rara vez están sujetas a la herencia y al upcast que se ven en las clases «ordinarias», casi nunca se verán funciones virtuales en clases contenedoras. La reutilización de las clases contenedoras se implementa mediante plantillas, no mediante herencia.