Disponible la nueva versión "donationware" 7.3 de OrganiZATOR
Descubre un nuevo concepto en el manejo de la información.
La mejor ayuda para sobrevivir en la moderna jungla de datos la tienes aquí.

Curso C++

[Home]  [Inicio]  [Índice]


4.12 Plantillas

§1 Introducción

Hemos indicado ( 1.1) que en la programación clásica existía una clara diferenciación entre los datos y su manipulación, es decir, entre los datos y el conjunto de algoritmos para manejarlos. Los datos eran tipos muy simples ( 2.2.1) y generalmente los algoritmos estaban agrupados en funciones orientadas de forma muy específica a los datos que debían manejar.

Posteriormente la POO introdujo nuevas facilidades; la posibilidad de extender el concepto de dato, permitiendo que existiesen tipos más complejos a los que se podía asociar la operatoria necesaria. Esta nueva habilidad fue perfilada con un par de mejoras adicionales: la posibilidad de ocultación de determinados detalles internos irrelevantes para el usuario, y la capacidad de herencia simple o múltiple [1].

Observe que las mejoras introducidas por la POO se pueden sintetizar en tres palabras: composición, ocultación y herencia. De otro lado, la posibilidad de incluir juntos los datos y su operatoria no era exactamente novedosa. Esta circunstancia ya existía de forma subyacente en todos los lenguajes. Recuerde que el concepto de entero (int en C) ya incluye implícitamente todo un álgebra y reglas de uso para dicho tipo. Observe también que la POO mantiene un paradigma de programación orientado al dato (o estructuras de datos). De hecho los "Objetos" se definen como instancias concretas de las clases y estas representan nuevos tipos-de-datos, de modo que POO es sinónimo de Programación Orientada a Tipos-de-datos [2].

§2 Programación genérica

Desde luego la POO supuso un formidable avance del arsenal de herramientas de programación. Incluso en algunos casos, un auténtico balón de oxígeno en el desarrollo y mantenimiento de aplicaciones muy grandes, en las que se estaba en el límite de lo factible con las técnicas programación tradicional. Sin embargo, algunos teóricos seguían centraron su atención en los algoritmos. Algo que estaba ahí también desde el principio. Se dieron cuenta que frecuentemente las manipulaciones contienen un denominador común que se repite bajo apariencias diversas. Por ejemplo, la idea de ordenación "Sort" se repite infinidad de veces en la programación, aunque los objetos a ordenar y los criterios de ordenación varíen de un caso a otro. Alrededor de esta idea surgió un nuevo paradigma denominado programación genérica o funcional.

La programación genérica está mucho más centrada en los algoritmos que en los datos y su postulado fundamental puede sintetizarse en una palabra: generalización. Significa que, en la medida de lo posible, los algoritmos deben ser parametrizados al máximo y expresados de la forma más independiente posible de detalles concretos, permitiendo así que puedan servir para la mayor variedad posible de tipos y estructuras de datos.

Los expertos consideran que la parametrización de algoritmos supone una aportación a las técnicas de programación, al menos tan importante, como fue en su momento la introducción del concepto de herencia, y que permite resolver algunos problemas que aquella deja sin solución.

Observe que la POO y la programación genérica representan enfoques en cierta forma ortogonales entre si:

La programación orientada al dato razona del siguiente modo: representemos un tipo de dato genérico (por ejemplo int) que permita representar objetos con ciertas características comunes (peras y manzanas). Definamos también que operaciones pueden aplicarse a este tipo (por ejemplo aritméticas) y sus reglas de uso, independientemente que el tipo represente peras o manzanas en cada caso.

Por su parte la programación funcional razona lo siguiente: construyamos un algoritmo genérico (por ejemplo sort), que permita representar algoritmos con ciertas características comunes (ordenación de cadenas alfanuméricas y vectores por ejemplo). Definamos también a que tipos pueden aplicarse a este algoritmo y sus reglas de uso, independientemente que el algoritmo represente la ordenación de cadenas alfanuméricas o de vectores.

Con el fin de adoptar los paradigmas de programación entonces en vanguardia, desde sus inicios C++ había adoptado conceptos de lenguajes anteriores. Uno de ellos, la programación estructurada [5], ya había sido recogida en el diseño de su antecesor directo C. También adoptó los conceptos de la POO entonces emergente ( 0.Iw1). Posteriormente ha incluido otros conceptos con que dar soporte a los nuevos enfoques de la programación funcional; básicamente plantillas y contenedores. Las plantillas, que se introdujeron con la versión del Estándar de Julio de 1998 son un concepto tomado de Ada. Los contenedores no están definidos en el propio lenguaje, sino en la Librería Estándar.

§3 Sinopsis

Las plantillas ("Templates"), también denominadas tipos parametrizados, son un mecanismo C++ que permite que un tipo pueda ser utilizado como parámetro en la definición de una clase o una función.

Ya se trate de clases o funciones, la posibilidad de utilizar un tipo como parámetro en la definición, posibilita la existencia de entes de nivel de abstracción superior al de función o clase concreta. Podríamos decir que se trata de funciones o clases genéricas; parametrizadas (de ahí su nombre). Las "instancias" concretas de estas clases y funciones conforman familias de funciones o clases relacionadas por un cierto "denominador común", de forma que proporcionan un medio simple de representar gran cantidad de conceptos generales y un medio sencillo para combinarlos.

Por ejemplo, supongamos las dos definiciones siguientes:

void foo (int i) { ... }                        // L.1

template < class T >  void foo (T t) { ... }    // L.2

La primera (L.1) es la definición de una función tradicional que recibe un entero, la segunda (L.2) es una función genérica que recibe un objeto de tipo T. Más tarde, en el código podríamos hacer:

int i = 128;    // una "instancia" de la clase de los enteros de valor 128

foo (i)         // invocación de foo de L.1

A a;            // una instancia de la clase A

foo (a);        // invocación de una versión para tipos A de foo de L.2

B b;            // una instancia de la clase B

foo (b);        // invocación de una versión para tipos B de foo de L.2

Punto importante a entender aquí es que en realidad, foo(a) y foo(b) son invocaciones a funciones distintas, fabricadas por el compilador a partir de la definición (plantilla) L.2


Para ilustrarlo intentaremos una analogía: si la clase Helado-de-Fresa representara todos los helados de fresa, de los que las "instancias" concretas serían distintos tamaños y formatos de helados de este sabor, una plantilla Helado-de-<tipo> sería capaz de generar las clases Helado-de-fresa; Helado-de-vainilla; Helado-de-chocolate, Etc. con solo cambiar adecuadamente el argumento <tipo>. En realidad respondería al concepto genérico de "Helado-de". Las instancias concretas de la plantilla forman una familia de productos relacionados (helados de diversos sabores). Forzando al máximo la analogía diríamos "especialidades".

Advertiremos desde ahora que el mecanismo de plantillas C++ es en realidad un generador de código parametrizado. La conjunción de ambas capacidades: generar tipos (datos) y código (algoritmos) les confiere una extraordinaria potencia. Si bien el propio inventor del lenguaje reconoce que a costa de "cierta complejidad", debida principalmente a la variedad de contextos en los que las plantillas pueden ser definidas y utilizadas [3].


La idea central a resaltar aquí es que una plantilla genera la definición de una clase o de una función mediante uno o varios parámetros. A esta instancia concreta de la clase o función generada, se la denomina especialización o especialidad de la plantilla.

Nota: un aspecto crucial del sistema es que los parámetros de la plantilla pueden ser a su vez plantillas.


Para manejar estos conceptos utilizaremos la siguiente terminología:

Clase-plantilla ("template class") o su equivalente: clase genérica.

Función-plantilla ("template function") o su equivalente: función genérica.

Instanciación de la plantilla
Clase genérica + argumento/s clase concreta (especialización)
Función genérica + argumento/s función concreta (especialización)


Como se ha indicado, las plantillas representan una de las últimas implementaciones del lenguaje y constituyen una de las soluciones adoptadas por C++ para dar soporte a la programación genérica. Aunque inicialmente fueron introducidas para dar soporte a las técnicas que se necesitaban para la Librería Estándar (para lo que se mostraron muy adecuadas), son también oportunas para muchas situaciones de programación. Precisamente la exigencia fundamental de diseño de la citada librería era lograr algoritmos con el mayor grado de abstracción posible, de forma que pudieran adaptarse al mayor número de situaciones concretas.

El tiempo ha demostrado que sus autores realizaron un magnífico trabajo que va más allá de la potencia, capacidad y versatilidad de la Librería Estándar C++ (STL 5.1) y de que otros lenguajes hayan seguido la senda marcada por C++ en este sentido. Por ejemplo Java, con su JGL ("Java Generic Library"). Lo que comenzó como una herramienta para la generación parametrizada de nuevos tipos de datos (clases), se ha convertido por propio derecho en un nuevo paradigma, la metaprogramación (programas que escriben programas).

§4 Versiones explícitas e implícitas

De lo dicho hasta ahora puede deducirse que las funciones y clases obtenidas a partir de versiones genéricas (plantillas), pueden obtenerse también mediante codificación manual (en realidad no se diferencian en nada de estas últimas). Aunque en lo tocante a eficacia y tamaño del código, las primeras puedan competir en igualdad de condiciones con las obtenidas manualmente. Esto se consigue porque el uso de plantillas no implica ningún mecanismo de tiempo de ejecución (runtime). Las plantillas dependen exclusivamente de las propiedades de los tipos que utiliza como parámetros y todo se resuelve en tiempo de compilación [4].

No existe inconveniente para la coexistencia en un programa de ambos tipos de código; el generado automáticamente por el mecanismo de plantillas y el generado de forma manual. Nos referiremos a ellos como especialidades generadas automáticamente, y generadas por el usuario; también como versiones explícitas (codificadas manualmente) e implícitas (generadas por el compilador). Veremos que para ciertos efectos el compilador puede distinguir entre unas y otras ( 4.12.1b).

Las plantillas representan un método muy eficaz de generar código (definiciones de funciones y clases ) a partir de definiciones relativamente pequeñas. Además su utilización permite técnicas de programación avanzadas, en las que implementaciones muy sofisticadas se muestran mediante interfaces que ocultan al usuario la complejidad, mostrándola solo en la medida en que necesite hacer uso de ella. De hecho, cada una de las potentes abstracciones que se utilizan en la Librería Estándar está representada como una plantilla. A excepción de algunas pocas funciones, prácticamente el 100% de la Librería Estándar está relacionada con las plantillas.

§5 template (palabra-clave)

C++ utiliza una palabra clave específica template para declarar y definir funciones y clases genéricas. En estos casos actúa como un especificador de tipo y va unido al par de ángulos < > que delimitan los argumentos de la plantilla:

template <T> void fun(T& ref);     // declaración de función genérica
template <T> class C {/*...*/};    // declaración de clase genérica


En algunas otras (raras) ocasiones, la palabra template se utiliza como calificador para indicar que determinada entidad es una plantilla (y en consecuencia puede aceptar argumentos) cuando el compilador no puede deducirlo por sí mismo. Ver ejemplo ( 5.1.1e1):

bs2.template to_string <char, char_traits<char>, allocator<char> >();

En este caso se trata de la instanciación explícita de una función genérica (método de clase) to_string que no acepta argumentos.

§6 Webografía

  Generic Programming Techniques.

  Inicio.


[1] Por supuesto no son los únicos, pero si los más importantes. Detalles como la sobrecarga de funciones son de menor importancia conceptual; representan más bien una facilidad léxica de los lenguajes que lo soportan. El creador del lenguaje C++ nos indica al respecto: "Thus overloaded function names are primarily a notational convenience. ... When a name is semantically significant, this convenience becomes essential."

[2]  Naturalmente estas cuestiones son relativas, son afirmaciones que pretenden transmitir ciertos conceptos y no verdades absolutas. Además siempre pueden ser llevadas al extremo; se puede argüir que una clase puede ser puro algoritmo, incluso es fácil proponer un ejemplo demostrativo:

class Cuadrado {
  public: static int cuad (int i) { return i * i; }
};
...
int x = 2;
cout << Cuadrado::cuad(x);

Sin embargo, se trata de un caso en el que el concepto de clase ha sido sacado de contexto tratando de emular una triste función de toda la vida.

[3] Stroustrup TC++PL C.13. N.del Autor: cuando el Sr. Stroustrup habla de "cierta complejidad" se debe estar preparado para lo peor :-)

[4]  El hecho de que sean resueltas en tiempo de compilación hace que no se pueda obtener el "tipo" de una plantilla con el operador typeid ( 4.9.14); en cualquier caso hay que aplicarlo sobre una instancia concreta.

[5] Un estilo de organización del código fuente introducido en los 70, que hacía más fácil de entender el flujo de ejecución del programa. Se caracteriza principalmente por utilizar sangrados y huir de saltos o "gotos" que hasta entonces habían sido muy frecuentes.