Tabla de contenidos
La herencia y la composición proporcionan una forma de reutilizar 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 introducidas en el lenguaje, parecían oponerse al uso de las jerarquías de clases contenedoras basadas en objetos (demostradas al final del Capítulo 15). Por ejemplo, los contenedores y algoritmos del C++ Estándar (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 las plantillas, también es una introducción a los contenedores, que son componentes fundamentales de la programación orientada a objetos y que se materializan casi por completo en los contenedores de la Librería Estándar de C++. Se verá que este libro ha estado usando ejemplos de contenedores (Stash y Stack) a lo largo de todo el texto, precisamente para que el lector se familiarice con los contenedores; en este capítulo se añadirá también el concepto de iterator. Aunque los contenedores son ejemplos ideales para usar con las plantillas, en el Volumen 2 (que tiene un capítulo de plantillas avanzadas) se aprenderá que también hay muchos otros usos para las plantillas.
Supóngase que se quiere crear una pila, como se ha estado haciendo a lo largo del 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ículo, como en la clase Stack que se ha examinado a lo largo 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 de los conejos. Aquí está el fichero 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
Ésta 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 se salta por completo el bucle while.
Obviamente, una pila de enteros no es una herramienta crucial. La necesidad real de los contenedores viene cuando se empiezan a crear objetos en el montículo usando new y se destruyen con delete. En un problema general de programación no se sabe cuántos 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 computador, se están manejando montones de formas, pero únicamente el usuario determina (en tiempo de ejecución) cuántas formas serán necesarias exactamente. Una vez que se aprecie esta tendencia, se descubrirán montones de ejemplos en las propias situaciones de programación.
Los programadores de C que dependen de la memoria virtual para manejar su «gestión de memoria» encuentran a menudo inquietante la idea de new, delete y de las clases contenedoras. Aparentemente, una práctica en C es crear un enorme array global, más grande que cualquier cosa que el programa parezca necesitar. Puede que esto no requiera pensar demasiado (ni conocer malloc() y free()), pero produce programas que no se pueden portar bien y que esconden errores sutiles.
Además, si se crea un enorme array global de objetos en C++, la sobrecarga de los constructores y de los destructores puede ralentizar 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 absolutamente necesarios. 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 punteros a algunos objetos de interés. Se crearán esos objetos usando new y se pondrá el puntero resultante en el contenedor (potencialmente haciendo upcasting en el proceso), y más tarde se sacará cuando se quiera hacer algo con el objeto. Esta técnica produce el tipo de programas más flexible y general.