Templates C++ Escrito por Operata Los templates son una inclusion posterior al lenguaje C++, que siempre han sido mas misteriosos para mi que el resto del lenguaje. Este texto de una universidad explica el tema. La generalidad es una propiedad que permite definir una clase o una función sin tener que especificar el tipo de todos o alguno de sus miembros. Esta propiedad no es imprescindible en un lenguaje de programación orientado a objetos y ni siquiera es una de sus características. Esta característica del C++ apareció mucho más tarde que el resto del lenguaje, al final de la década de los ochenta. Esta generalidad se alcanza con las plantillas (templates). La utilidad principal de este tipo de clases o funciones es la de agrupar variables cuyo tipo no esté predeterminado. Así el funcionamiento de una pila, una cola, una lista, un conjunto, un diccionario o un array es el mismo independientemente del tipo de datos que almacene (int, long, double, char, u objetos de una clase definida por el usuario). En definitiva estas clases se definen independientemente del tipo de variables que vayan a contener y es el usuario de la clase el que debe indicar ese tipo en el momento de crear un objeto de esa clase. 1 Introducción Hemos indicado 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, 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 elconcepto 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 cultación de determinados detalles internos, irrelevantes para el usuario, y lacapacidad de herencia simple o múltiple. 1 / 11 Templates C++ Escrito por Operata 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 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 recuentemente 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 no puede resolver. 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 por ejemplo). Definamos también que operaciones pueden 2 / 11 Templates C++ Escrito por Operata 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 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 , ya había sido recogida en el diseño de su antecesor directo C. También adoptó los conceptos de la POO entonces emergente . 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 del Ada. Los contenedores no están definidos en el propio lenguaje, sino en la Librería Estándar. 2 Concepto de plantilla 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 3 / 11 Templates C++ Escrito por Operata proporcionan un medio simple de representar gran cantidad de conceptos generales y un medio sencillo para combinarlos. Para ilustrarlo intentaremos una analogía: si la clase Helado-de-Fresa representara 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 - sería capaz de generar las clases Heladode-fresa; Helado-de-vainilla; Helado-de-chocolate, etc. con solo cambiar adecuadamente el argumento . 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". Observe 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. 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 se la denomina una especialización o especialidad de la plantilla. 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: · Plantilla de clase (template class) o su equivalente: clase genérica. · Plantilla de función (template function) o su equivalente: función genérica. Definida una plantilla, al proceso por el cual se obtienen clases especializadas (es decir para alguno de los tipos de datos específicos) se denomina instanciación o de la plantilla o especialización de una clase o método genérico. A las funciones y clases generadas para determinados tipos de datos se las denomina entonces como clases o funciones concretas o especializadas. Como se ha indicado, las plantillas representan una de las últimas implementaciones del lenguaje y constituyen una de las soluciones adoptadas por C++ para 4 / 11 Templates C++ Escrito por Operata 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 parece demostrar 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++ y de que otroslenguajes hayan seguido la senda marcada por C++ en este sentido. Tal es el caso de 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). 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. Las plantillas dependen exclusivamente de las propiedades de los tipos que utiliza como parámetros y todo se resuelve en tiempo de compilación. Podríamos pensar que su resolución es similar a las macros de C en el que se hacía una sustitución textual de la expresión indicada en el define por la expresión puesta a continuación, pero en función de unos parámetros. De igual forma, cuando se especializa una plantilla, se escribe el código con los tipos sustitudos por lo indicado en la plantilla, y después se procede a compilar tanto el código escrito manualmente como el escrito automáticamente por este mecanismo. 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, de ahí que hasta ahora no se halla hecho mucha referencia a esta librería perteneciente al estándar de C++. La palabra clave template C++ utiliza una palabra clave específica template para declarar y definir funciones y 5 / 11 Templates C++ Escrito por Operata 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 void fun(T& ref); // función genérica template class C {/*...*/}; // clase genérica En algunas otras (raras) ocasiones la palabra template se utiliza como calificador para indicar que eterminada entidad es una plantilla (y en consecuencia puede aceptar argumentos) cuando el compilador no puede deducirlo por sí mismo. Bien, pues una vez expuestas las ideas principales referentes al concepto de plantilla, vamos a ver en primer lugar como se realzan funciones genéricas, para después explicar el concepto y el modo de funcionamiento de las clases genéricas. 3 Plantillas de funciones Para ilustrar gráficamente su utilidad utilizaremos un ejemplo clásico: queremos construir una función max(a, b) que pueda utilizarse para obtener el mayor de dos valores, suponiendo que estos sean de cualquier tipo capaz de ser ordenado, es decir, cualquier tipo en el que se pueda establecer un criterio de ordenación (establecemos a > b si a está después que b en el orden). El problema que presenta C++ para esta propuesta es que al ser un lenguaje fuertemente tipado, la declaración c max(a, b) requiere especificar el tipo de argumentos y valor devuelto. En realidad se requiere algo así: tipoT max(tipoT a, tipoT b); y la sintaxis del lenguaje no permite que tipoT sea algo variable. Una posible solución es sobrecargar la función max(), definiendo tantas versiones como tipos distintos debamos utilizar. 6 / 11 Templates C++ Escrito por Operata double max(double a,double b){return a>b?a:b;} int max(int a,int b){return a>b?a:b;} … Otra alternativa sería utilizar una macro: #define max(a, b) ((a > b) ? a : b) pero esto presenta sus inconvenientes. Empezando porque su utilización permitiría comparar un entero con una estructura o una matriz, algo que está claramente fuera del propósito de la función que pretendemos. La solución al problema enunciado es utilizar una función genérica (plantilla). La sintaxis de su definición es la siguiente: template T max(T a, T b) { return (a > b) ? a : b; } es la lista de parámetros. Representa el/los parametros de la plantilla. Los parámetros de una plantilla funciona en cierta forma como los argumentos de una macro (el trabajo de esta macro es generar código de funciones). Es importante significar que utilizamos dos conceptos distintos (aunque relacionados): los parámetros de la plantilla (contenidos en la lista template ) y los argumentos de la función (argumentos con que se invoca la función en cada caso concreto). 7 / 11 Templates C++ Escrito por Operata Lo mismo que en las funciones explícitas, las genéricas pueden ser declaradas antes de su utilización: template T max(T, T); y definidas después: template T max(T a, T b) { return (a > b) ? a : b; } La idea fundamental es que el compilador deduce los tipos concretos de los parámetros de la plantilla de la inspección de los argumentos actuales utilizados en la invocación . Por ejemplo, la plantilla anterior puede ser utilizada mediante las siguientes sentencias: int i, j; UnaClase a, b; ... int k = max(i, j); // (1) UnaClase c = max(a, b); // (2) 8 / 11 Templates C++ Escrito por Operata En (1) los argumentos de la función son dos objetos tipo int; mientras en (2) son dos objetos tipo UnaClase. El compilador es capaz de construir dos funciones aplicando los parámetros adecuados a la plantilla. En el primer caso, el parámetro es un int; en el segundo un tipo UnaClase. Como veremos más adelante, es de la máxima importancia que el compilador sea capaz de deducir los parámetros de la plantilla a partir de los argumentos actuales (los utilizados en cada invocación de la función), así como las medidas sintácticas adoptadas cuando esto no es posible por producirse ambigüedad. Una función genérica puede tener más argumentos que la plantilla. Por ejemplo: template void func(T, inf, char, long, ...); También puede tener menos: template void func(); La forma de operar en este caso para que el compilador deduzca el parámetro correcto T a utilizar en la plantilla, se muestra más adelante cuando se hable de la especificación explícita de los argumentos de una plantilla. Llegados a este punto es conveniente hacer algunas observaciones importantes: Las funciones genéricas son entes de nivel de abstracción superior a las funciones concretas (en este contexto preferimos llamarlas funciones explícitas), pero las funciones genérica s solo tienen existencia en el código fuente y en la mente del programador. Hemos dicho que el mecanismo de plantillas C++ se resuelve en tiempo de compilación, de modo que en el ejecutable, y durante la ejecución, no existe nada parecido a una función genérica , solo existen especializaciones (instancias de la función genérica). Esta característica de las funciones genéricas es de la mayor importancia. Supone que pueden escribirse algoritmos muy genéricos en los que los detalles dependen del tipo de objeto con el que se utiliza (el algoritmo). En nuestro ejemplo, el criterio que define que objeto a o b es mayor, no está contenido en la función max(), sino en la propia clase a que pertenecen ambos objetas en donde ha debido sobrecargarse el operador > para ese tipo concreto. Esta es justamente la premisa fundamental de la programación genérica. La instanciación de la plantilla se produce cuando el compilador encuentra que es necesaria una versión concreta (especialidad) de la función genérica. Esto sucede cuando existe una 9 / 11 Templates C++ Escrito por Operata invocación como en el ejemplo, la línea (2) , o se toma la dirección de la función (por ejemplo para iniciar un puntero-a-función). Entonces se genera el código apropiado en concordancia con el tipo de los argumentos actuales. Ocurre que si esta instancia aparece más de una vez en un módulo, o es generada en más de un módulo, el enlazador las refunde automáticamente en una sola definición, de forma que solo exista una copia de cada instancia. Dicho en otras palabras: en la aplicación resultante solo existirá una definición de cada función. Por contra, si no existe ninguna invocación no se genera ningún código. Aunque la utilización de funciones genéricas conduce a un código elegante y reducido, que no se corresponde con el resultado final en el ejecutable. Si la aplicación utiliza muchas plantillas con muchos tipos diferentes, el resultado es la generación de gran cantidad de código con el consiguiente consumo de espacio. Esta crecimiento del código es conocida como "Code bloat", y puede llegar a ser un problema. En especial cuando se utilizan las plantillas de la Librería Estándar, aunque existen ciertas técnicas para evitarlo. Como regla general, las aplicaciones que hace uso extensivo de plantillas resultan grandes consumidoras de memoria (es el costo de la comodidad). Puesto que cada instancia de una función genérica es una verdadera función, cada especialización dispone de su propia copia de las variables estáticas locales que hubiese. Se les pueden declarar punteros y en general gozan de todas las propiedades de las funciones normales, incluyendo la capacidad de sobrecarga Veamos un caso concreto con una función genérica que utiliza tanto una clase Vector como un entero: #include class Vector { public: float x, y; bool operator>(Vector v) 10 / 11 Templates C++ Escrito por Operata { return ((x*x + y*y) > (v.x*v.x + v.y*v.y))? true: false; } }; template T max(T a, T b){ return (a > b) ? a : b;} void main() { Vector v1 = {2, 3}, v2 = {1, 5}; Int x = 2, y = 3; cout 11 / 11