16.8. Por qué usar iteradores

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 virtuales. 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.

16.8.1. Plantillas Función

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.