16: Introducción a las Plantillas

Tabla de contenidos

16.1. Contenedores
16.2. Un vistazo a las plantillas
16.3. Sintaxis del Template
16.4. Stack y Stash como Plantillas
16.5. Activando y desactivando la propiedad
16.6. Manejando objetos por valor
16.7. Introducción a los iteradores
16.8. Por qué usar iteradores
16.9. Resumen
16.10. Ejercicios

La herencia y la composición proporcionan una forma de retilizar código objeto. Las plantillas de C++ proporcionan una manera de reutilizar el código fuente.

Aunque las plantillas (o templates) son una herramienta de programación de propósito general, cuando fueron introducidos en el lenguaje, parecían oponerse al uso de las jerarquías de clases contenedoras basadas en objetos (demostrado al final del Capítulo 15). Además, los contenedores y algoritmos del C++ Standard (explicados en dos capítulos del Volumen 2 de este libro, que se puede bajar de www.BruceEckel.com) están construidos exclusivamente con plantillas y son relativamente fáciles de usar por el programador.

Este capítulo no sólo muestra los fundamentos de los templates, también es una introducción a los contenedores, que son componentes fundamentales de la programación orientada a objetos lo cual se evidencia a través de los contenedores de la librería estándar de C++. Se verá que este libro ha estado usando ejemplos contenedores - Stash y Stack- para hacer más sencillo el concepto de los contenedores; en este capítulo se sumará el concepto del iterator. Aunque los contenedores son el ejemplo ideal para usarlos con las plantillas, en el Volumen 2 (que tiene un capítulo con plantillas avanzadas) se aprenderá que también hay otros usos para los templates.

16.1. Contenedores

Supóngase que se quiere crear una pila, como se ha estado haciendo a través de este libro. Para hacerlo sencillo, esta clase manejará enteros.

//: C16:IntStack.cpp
// Simple integer stack
//{L} fibonacci
#include "fibonacci.h"
#include "../require.h"
#include <iostream>
using namespace std;

class IntStack {
  enum { ssize = 100 };
  int stack[ssize];
  int top;
public:
  IntStack() : top(0) {}
  void push(int i) {
    require(top < ssize, "Too many push()es");
    stack[top++] = i;
  }
  int pop() {
    require(top > 0, "Too many pop()s");
    return stack[--top];
  }
};

int main() {
  IntStack is;
  // Add some Fibonacci numbers, for interest:
  for(int i = 0; i < 20; i++)
    is.push(fibonacci(i));
  // Pop & print them:
  for(int k = 0; k < 20; k++)
    cout << is.pop() << endl;
} ///:~

Listado 16.1. C16/IntStack.cpp


La clase IntStack es un ejemplo trivial de una pila. Para mantener la simplicidad ha sido creada con un tamaño fijo, pero se podría modificar para que automáticamente se expanda usando la memoria del montón, como en la clase Stack que ha sido examinada a través del libro.

main() añade algunos enteros a la pila, y posteriormente los extrae. Para hacer el ejemplo más interesante, los enteros son creados con la función fibonacci(), que genera los tradicionales números de la reproducción del conejo. Aquí está el archivo de cabecera que declara la función:

//: C16:fibonacci.h
// Fibonacci number generator
int fibonacci(int n); ///:~

Listado 16.2. C16/fibonacci.h


Aquí está la implementación:

//: C16:fibonacci.cpp {O}
#include "../require.h"

int fibonacci(int n) {
  const int sz = 100;
  require(n < sz);
  static int f[sz]; // Initialized to zero
  f[0] = f[1] = 1;
  // Scan for unfilled array elements:
  int i;
  for(i = 0; i < sz; i++)
    if(f[i] == 0) break;
  while(i <= n) {
    f[i] = f[i-1] + f[i-2];
    i++;
  }
  return f[n];
} ///:~

Listado 16.3. C16/fibonacci.cpp


Esta es una implementación bastante eficiente, porque nunca se generan los números más de una vez. Se usa un array static de int, y se basa en el hecho de que el compilador inicializará el array estático a cero. El primer bucle for mueve el índice i a la primera posición del array que sea cero, entonces un bucle while añade números Fibonacci al array hasta que se alcance el elemento deseado. Hay que hacer notar que si los números Fibonacci hasta el elemento n ya están inicializados, entonces también se salta el bucle while.

16.1.1. La necesidad de los contenedores

Obviamente, una pila de enteros no es una herramienta crucial. La necesidad real de los contenedores viene cuando se empizan a crear objetos en el montón (heap) usando new y se destruyen con delete. En un problema general de programación no se saben cuantos objetos van a ser necesarios cuando se está escribiendo el programa. Por ejemplo, en un sistema de control de tráfico aéreo no se quiere limitar el número de aviones que el sistema pueda gestionar. No puede ser que el programa se aborte sólo porque se excede algún número. En un sistema de diseño asistido por computadora, se están manejando montones de formas, pero únicamente el usuario determina (en tiempo de ejecución) cuantas formas serán necesarias. Una vez apreciemos estas tendencias, se descubrirán montones de ejemplos en otras situaciones de programación.

Los programadores de C que dependen de la memoria virtual para manejar su "gestión de memoria" encuentran a menudo como perturbantentes las ideas del new, delete y de los contenedores de clases. Aparentemente, una práctica en C es crear un enorme array global, más grande que cualquier cosa que el programa parezca necesitar. Para esto no es necesario pensar demasiado (o hay que meterse en el uso de malloc() y free()), pero se producen programas que no se pueden portar bien y que esconden sutiles errores.

Además, si se crea un enorme array global de objetos en C++, la sobrecarga de los constructores y de los destructores pueden enlentecer las cosas de forma significativa. La aproximación de C++ funciona mucho mejor: Cuando se necesite un objeto, se crea con new, y se pone su puntero en un contenedor. Más tarde, se saca y se hace algo con él. De esta forma, sólo se crean los objetos cuando sea necesario. Y normalmente no se dan todas las condiciones para la inicialización al principio del programa. new permite esperar hasta que suceda algo en el entorno para poder crear el objeto.

Así, en la situación más común, se creará un contenedor que almacene los punteros de algunos objetos de interés. Se crearán esos objetos usando new y se pondrá el puntero resultante en el contenedor (potencialmete haciendo upcasting en el proceso), más tarde el objeto se puede recuperar cuando sea necesario. Esta técnica produce el tipo de programas más flexible y general.