"Ningún juego dura tanto hasta que te lo terminas como el que te programas tú mismo,
ninguno te absorbe tanto y ninguno te produce tanta satisfacción cuando lo has acabado"
Programacion de videojuegos
Martes 19 de Noviembre del 2019

Menu principal
Colaborar (con PayPal)

Para continuar con el trabajo de esta Web y poder pagar el hosting, viene bien la ayuda que sea. Gracias a todos.

Importe: 

Ultimas descargas
19.Jan

Clase que permite dibujar texto en OpenGL con mucha facilidad.Usa FreeType2.Para ver que hace y c...


Templates C++ Imprimir Correo electrónico
Manuales varios - Manuales de Programación
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.

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

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

 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éricas 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 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)

       {

             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 <<"Mayor: "<<< endl;

cout <<"Mayor:"<<<", "<<< endl;

}

 

El resultado de ejecutar el programa es el siguiente:

 Mayor: 3

Mayor: 1, 5

 Otro ejemplo clásico en la definición de una plantilla de función es el caso de la función permutar:

 #include

template void permutar(S&, S&);

void main(void)

{

int i=2, j=3;

cout << "i=" << i << " " << "j=" << j << endl;

permutar(i, j);

cout << "i=" << i << " " << "j=" << j << endl;

double x=2.5, y=3.5;

cout << "x=" << x << " " << "y=" << y << endl;

permutar(x, y);

cout << "x=" << x << " " << "y=" << y << endl;

}

 

template void permutar(S& a, S& b)

{

S temp;

temp = a;

a = b;

b = temp;

}

 

Otro ejemplo, es el de la realización de un algoritmo de ordenación por selección directa. En este caso se hará uso de la función genérica permutar. Observese que a causa de ello, el numero de funciones generadas automáticamente pasa a ser cuatro. Si se implementase el código de la función genérica permutar directamente en el interior de la función genérica ordenar, el número de funciones generadas automáticamente hubiera sido la mitad:

 

#include

template void permutar(S& a, S& b);

template void ordenar (T *vector, int num);

 

void main(void)

{

float mivector[10]={2,4,6,8,1,3,5,7,9,0};

char cadena[10]="efghBACDI";

ordenar(mivector,8);

ordenar(cadena,10);

for(i=0;i<10;i++)cout<

cout<<

}

 

template void permutar(S &a, S &b)

{

S temp;

temp = a;

a = b;

b = temp;

}

 

template void ordenar (T *vector, int num)

{

int i,j;

for(i=0;i

for(j=i+1;j

if(vector[i]

}

 

3.1 Metodos genéricos

Las funciones genéricas pueden ser miembros (métodos) de clases:

 

class A

{

template void func(T& t) // def de método genérico

{

...

}

...

}

 

La definición de métodos genéricos puede hacerse también fuera del cuerpo de la clase como con cualquier otro método, mediante el uso del operador de resolución de ámbito:

 

class A

{

template void func(T& t);//decl método genérico

...

}

...

template void A::func(T& t) //definición del método

{

// código del método...

}

 

Aunque aún no se han explicado las clases genéricas, es conveniente indicar ya que los miembros genéricos pueden ser a su vez miembros de clases genéricas, en cuyo caso pueden hacer uso de los parámetros de la plantilla de clase genérica. Un ejemplo de un método genérico es el siguiente:

 

#include

class A

{

public:

int x;

template void fun(T t1, T t2);

A (int a = 0) { x = a; }

};

 

template void A::fun(T t1, T t2)

{

cout << "Valor-1: " << t1

<< ", Valor-2: " << t2

<< ", Valor-x: " << x << endl;

}

 

void main(void)

{

A a(7), b(14);

a.fun(2, 3);

b.fun('x', 'y');

}

 

Salida:

 

Valor-1: 2, Valor-2: 3, Valor-x: 7

Valor-1: x, Valor-2: y, Valor-x: 14

 

 

3.2 Parámetros de la plantilla

La definición de la función genérica puede incluir más de un argumento. Es decir, el especificador template puede contener una lista con varios tipos. Estos parámetros pueden ser tipos complejos o fundamentales, por ejemplo un int; incluso especializaciones de clases genéricas y constantes de tiempo de compilación.

En general los parámetros de una plantilla pueden ser:
  • Identificadores de tipo, por ejemplo T. Estos parámetros van precedidos por la palabra reservada class o typename:

 

template void func(A, B);

template void func(A,B);

 

  • Plantillas de clases (adelantando de nuevo):

 

template class X> void func(A);

X);

  • Parámetros de algún otro tipo: primitivo, derivado, definido por el usuario, o de plantilla. Un parámetro de plantilla que no sea un tipo, es una constante dentro de la plantilla, y por lo tanto, no se puede modificar.

 

template void func(A, int);

template T fx(T x);

 

Un ejemplo que ilustra este último caso, es el siguiente en el que se utilizan dos plantillas muy parecidas desde el punto de vista de uso, pero diferentes en cuanto al código compilado es el siguiente:

 

#include

template void imprime(T* v)

{

for(int i=0;i<

}

template void imprime2(T* v,int ind)

{

for(int i=0;i<

}

 

void main()

{

float vector[3]={1,2,3};

char cadena[]="Hola Mundo";

imprime(vector); //(1)

imprime(cadena);

imprime2(vector,2);

imprime2(cadena,5);

}

 

Los argumentos a la plantilla de las funciones genéricas no pueden tener valores por defecto. Lo mismo que en las funciones explícitas, en las funciones genéricas debe existir concordancia entre el número y tipo de los argumentos formales y actuales.

 

template void func(A, int);

...

func(T); // Error!! falta 1 argumento

 

Evidentemente los parámetros de la función, no afectados por la plantilla, si que pueden tener valores por defecto. Por ejemplo, la función imprime2 podría ser reescrita de la forma siguiente:

 

template void imprime2(T* v,int ind=2)

{

for(int i=0;i<

}

 

y a la hora de ejecutarse podría entonces escribirse:

 

imprime2(vector);

 

Como se observa en el ejemplo, todos los argumentos formales de la plantilla (contenidos en la lista template ) deberían estar representados en los argumentos formales de la función. De no ser así, no habría posibilidad de deducir los valores de los tipos de la lista salvo que se indique explícitamente cuando se produzca una invocación específica de la función por medio del uso de –como en la línea (1) del ejemplo de imprime-. El siguiente código por tanto daría error en la compilación:

 

template void func(A a) { // Plantilla

...

};

...

func(a); // Error de compilación !!

 

En este caso el compilador no tiene posibilidad de deducir el tipo del argumento B. En ocasiones el diseño de la función no permite determinar el tipo de parámetro de la plantilla a partir de los argumentos actuales de la invocación (o sencillamente se quiere obligar al compilador a aplicar los argumentos actuales a una especialización instanciada con unos parámetros concretos). Por ejemplo:

 

template T* construir ()

{

T *aux=new T;

if(aux==NULL)

{

cout<<”Error de construcción”;

abort();

}

return aux;

}

 

La plantilla anterior crea un objeto de cualquier tipo y devuelve un puntero al objeto creado, o aborta el programa en caso de no haber podido crearlo. Se observa que el compilador no puede deducir el tipo de parámetro a utilizar con la plantilla a partir del argumento actual, puesto que en este caso no hay argumento actual. La siguiente línea sería errónea:

 

int* iptr = construir();

 

El compilador arrojaría el mensaje: ERROR, parámetro T desconocido. Para su utilización debe especificarse explícitamente el tipo de parámetro a utilizar mediante un argumento de plantilla:

 

int* iptr = construir();

 

La gramática del lenguaje exige que cuando el argumento de la plantilla solo es utilizado por la función en el tipo de retorno, debe indicarse explícitamente que tipo de instanciación se pretende mediante la inclusión de parámetros de plantilla entre el nombre de la función y la lista de argumentos: función (...). Esto es coherente con el modo de comportarse gramaticalmente el compilador con las funciones sobrecargadas. Recuérdese que para estas funciones, la diferenciación entre las mismas no puede realizarse por medio del tipo de la función o de retorno.

Esta forma de instanciar una plantilla se denomina instanciación implícita específica de la plantilla o especificación explícita de los parámetros de la plantilla, y en estos casos la lista que sigue al nombre de la función genérica puede incluir los parámetros de la plantilla que sean necesarios. Esta lista no tiene porqué incluir a todos los parámetros actualmente utilizados por la plantilla, ya que el compilador la completa con los tipos que puedan deducirse de la lista de argumentos de la función. Sin embargo, los parámetros faltantes deben ser los últimos de la lista (análogo a lo exigido a los argumentos por defecto en las funciones explícitas ). Por ejemplo:

 

template void func(B b, C c, int i);

....

func(b, c, i); // Error!!

func (b, c, i); // Ok. B y C redundantes

func(b, c, i); // Ok.

func(b, c); // Error!! falta argumento i

 

Un aspecto crucial de las funciones genéricas es que el compilador debe poder deducir sin ambigüedad los argumentos de la plantilla a partir de los argumentos utilizados para la invocación de la función.
Recordemos que en los casos de sobrecarga, la invocación de funciones C++ utiliza un sistema estándar para encontrar la definición que mejor se adapta a los argumentos actuales. También se realizan transformaciones automáticas cuando los argumentos pasados a la función no concuerdan exactamente con los esperados (argumentos formales) Estos mecanismos utilizan unas reglas denominadas congruencia estándar de argumentos En caso de las plantillas de función o funciones genéricas, el compilador deduce los parámetros de la plantilla mediante el análisis de los argumentos actuales de la invocación, pero para esto solo realiza conversiones triviales (menos significativas que las realizadas con las funciones explícitas). Los siguientes ejemplos pueden ayudar a ilustrar estas conversiones tanto para una función genérica como para una normal o explícita:

 

template bool igual(T a, T b)

{

return (a == b) ? true : false;

}

 

bool desigual(double a, double b)

{

return (a == b) ? false : true;

}

...

 

int i;

char c;

double d;

...

igual(i, i); // Ok. invoca igual(int ,int)

igual(c, c); // Ok. invoca igual(char,char)

igual(i, c); // Error!! igual(int,char) indefinida

igual(c, i); // Error!! igual(char,int) indefinida

desigual(i, i) // Ok. conversión de argumentos efectuada

desigual(c, c) // Ok. conversión de argumentos efectuada

desigual(i, c) // Ok. conversión de argumentos efectuada

desigual(d, d) // Ok. concordancia de argumentos

 

3.3 Sobrecarga de funciones genéricas

Hemos señalado que la instanciación de la plantilla se realiza cuando el compilador encuentra una invocación de la función genérica o se obtiene su dirección y que solo puede existir una versión de cada especialización de la función genérica. Estas premisas conducen a que sea posible evitar la generación automática para uno o varios tipos concretos, mediante dos procedimientos:
  • Proporcionando una versión codificada de forma "manual" de la función (versión explícita ). Es decir escribiendo el código de la función con los parámetros especificados normalmente.
  • Forzar una instanciación específica de la plantilla (instanciación explícita), de forma que se genera el código de una especialidad concreta, con independencia de que posteriormente se requiera o no, la utilización del código generado. La instanciación puede realizarse de dos formas: o Forzar la instanciación de la plantilla "tal cual" para un tipo particular. Esta instancia explícita tendría el comportamiento genérico definido en la plantilla, por lo que la denominamos instanciación explícita general. o Forzar una instanciación para un tipo particular en las mismas condiciones que el anterior (con independencia de la posible utilización del código generado en el programa), pero definiendo un nuevo comportamiento, distinto del general definido en la plantilla. En otras palabras: instanciar una versión sobrecargada de la función para un tipo específico. La denominamos instanciación explícita particular .

 

Como veremos a continuación, estas posibilidades tienen distinta utilidad y ligeras diferencias de detalle. Aunque son técnicas diferentes, el resultado final es análogo: la existencia de una (o varias) especializaciones concretas de la función, lo que nos obligará a contemplar una generalización de la sobrecarga de funciones que incluya funciones explícitas y genéricas. Con independencia de la explicación más detallada que sigue, para situarnos en el tema adelantamos un esbozo de lo que significa la literatura anterior referida a un caso muy sencillo:

 

// función genérica (declaración)

template T max(T, T);

...

// función genérica (definición)

template T max(T a, T b) { return (a > b) ? a : b; }

...

// versión explícita

char max(char a, char b) { return (a >= b) ? a : b; }

...

// instanciación explícita general

template T max(long a, long b);

...

// instanciación explícita particular

template T max(double a, double b)

{ return (a >= b) ? a : b; };

...

// instanciación implícita específica

int x = max(x, 'c');

 

Para entrar con mayor profundidad en el tema , considere un caso en el que utilizamos una función genérica igual( ) para comprobar si dos objetos son iguales, y a la cual iremos aplicando cada uno de los casos anteriores:

 

#include

class Vector

{

public:

float x, y;

Vector(float a,float b):x(a),y(b){}

bool operator==(const Vector& v)

{

return ( x == v.x && y == v.y)? true : false;

}

};

 

template bool igual(T a, T b) función genérica

{

return (a == b) ? true : false;

}

 

void main()

{

Vector v1(2, 3), v2 (1, 5);

int x = 2, y = 3;

double d1 = 2.0, d2 = 2.2;

if ( igual(v1, v2) ) cout << "vectores iguales" << endl;

else cout << "vectores distintos" << endl;

if ( igual(d1, d2) ) cout << "doubles iguales" << endl;

else cout << "doubles distintos" << endl;

if ( igual(x, y) ) cout << "enteros iguales" << endl;

else cout << "enteros distintos" << endl;

}

 

La salida de este programa, como era de esperar es la siguiente:

 

vectores distintos

doubles distintos

enteros distintos

 

Hasta aquí nada nuevo: el compilador ha generado y utilizado correctamente las especializaciones de igual( ) para las invocaciones con tipos int, double y Vector.

 

A. Versión explícita

Consideremos ahora que es necesario rebajar la exigencia para que dos variables sean consideradas iguales en el caso de que sean doubles. Para ello introducimos una instancia de igual codificada manualmente en el que reflejamos la nueva condición de igualdad:

 

#include

class Vector

{

public:

float x, y;

Vector(float a,float b):x(a),y(b){}

bool operator==(const Vector& v)

{

return ( x == v.x && y == v.y)? true : false;

}

};

 

template bool igual(T a, T b)

{

return (a == b) ? true : false;

};

 

bool igual(double a, double b) // versión explícita

{

return (labs(a-b) < 1.0) ? true : false;

};

 

void main()

{

Vector v1(2, 3), v2 (1, 5);

int x = 2, y = 3;

double d1 = 2.0, d2 = 2.2;

if ( igual(v1, v2) ) cout << "vectores iguales" << endl;

else cout << "vectores distintos" << endl;

if ( igual(d1, d2) ) cout << "doubles iguales" << endl;

else cout << "doubles distintos" << endl;

if ( igual(x, y) ) cout << "enteros iguales" << endl;

else cout << "enteros distintos" << endl;

}

 

Salida:

 

vectores distintos

doubles iguales

enteros distintos

 

La versión explícita para tipos double utiliza la función de librería labs para conseguir que dos doubles sean considerados iguales si la diferencia es solo en los decimales. La inclusión de esta definición supone que el compilador no necesita generar una versión de igual() cuando los parámetros son tipo double. En este caso, el compilador utiliza la versión suministrada "manualmente" por el programador. Además de permitir introducir modificaciones puntuales en el comportamiento general, las versiones explícitas pueden utilizarse también para eliminar algunas de las limitaciones de las funciones genéricas. Por ejemplo, si sustituimos la sentencia:

 

if ( igual(d1, d2) ) cout << "doubles iguales" << endl;

 

por:

 

if ( igual(d1, y) ) cout << "doubles iguales" << endl;

 

Se obtiene un error de compilación: Could not find a match for 'igual(double,int)'. La razón es que, como hemos visto, el compilador no realiza ningún tipo de conversión sobre el tipo de los argumentos utilizados en las funciones genéricas, y en este caso no existe una definición de igual() que acepte un double y un int. En cambio, la misma sustitución de cuando existe una versión explícita para igual(double double), no produce ningún error. La razón es que para las funciones normales el compilador si es capaz de realizar automáticamente determinadas transformaciones de los argumentos actuales para adecuarlos a los esperados por la función.

 

B. Instanciación explícita general

El estándar ha previsto un procedimiento para obligar al compilador a generar el código de una especialización concreta a partir de la plantilla-función. Esta instanciación forzada se denomina instanciación explícita, y utiliza el especificador template aislado (sin estar seguido de ). Recuerde que la definición de la plantilla igual es:

 

template bool igual(T a, T b) {...}

 

La sintaxis para generar una versión de igual específica para doubles sería la siguiente:

 

template bool igual(double a, double b);

 

Observe la sintaxis utilizada: la lista de parámetros se ha cambiado de posición respecto a la declaración de la plantilla.
La inclusión de una instanciación explícita como la anterior (la llamaremos general porque sigue el comportamiento general definido por la plantilla), origina la aparición del código correspondiente a la especialización solicitada aunque en el programa no exista una necesidad real (invocación) de dicho código. Esta instancia explícita general desempeña un papel análogo al de una versión que se hubiese codificado manualmente (versión explícita).

 

C. Instanciación explícita particular

Observe que la versión instanciada en la expresión anterior es concordante con la plantilla, por lo que no sirve para realizar modificaciones específicas como las realizadas en el ejemplo anterior en el caso de los float. Sin embargo, es posible también especificar una definición particular para la especialidad que se instancia añadiendo el cuerpo adecuado. A esta versión la denominamos instancia explícita particular. La sintaxis seria la siguiente:

 

template bool igual(double a, double b)

{

return (labs(a-b) < 1.0) ? true : false;

}

 

Los ángulos después de template indican al compilador que sigue una especialización particular de una plantilla definida previamente. Como puede figurarse el lector, el resultado es similar al que se obtendría una versión explícita .Es un error intentar la existencia de más de una definición para la misma función, ya sea esta una instanciación implícita, explícita o una versión codificada manualmente. Por ejemplo:

 

bool igual(double& a, double& b)

{

return (labs(a-b) < 1.0) ? true : false;

};

 

template bool igual(double& a, double& b);

 

En las condiciones anteriores el compilador puede generar un error, una advertencia, o sencillamente ignorar el segundo requerimiento, ya que previamente existe una versión explícita de la función con idéntica firma. En cualquier caso es una regla que el compilador dará preferencia a una función normal (versión explícita) sobre cualquier forma de instanciación, explícita o implícita, al utilizar una función.

 

4 Clases genéricas

Hemos indicado al comienzo del capítulo que las clases-plantilla, clases genéricas o generadores de clases, son un artificio C++ que permite definir una clase mediante uno o varios parámetros. Este mecanismo es capaz de generar la definición de clases (instancias o especializaciones de la plantilla) distintas, pero compartiendo un diseño común. Podemos imaginar que una clase genérica es un constructor de clases, que como tal acepta determinados argumentos (no confundir con el constructor de-una-clase, que genera objetos). Para ilustrarlo veremos la clase mVector. Los objetos mVector son matrices cuyos elementos son objetos de la clase Vector; que a su vez representan vectores de un espacio de dos dimensiones. El diseño básico de la clase es como sigue:

 

class mVector

{

int dimension;

public:

Vector* mVptr;

mVector(int n = 1)

{

dimension = n;

mVptr = new Vector[dimension];

}

~mVector() { delete [] mVptr; }

Vector& operator[](int i) { return mVptr[i]; }

void mostrar(int);

};

 

void mVector::mostrar (int i)

{

if((i >= 0) && (i <= dimension)) mVptr[i].mostrar();

}

 

El sistema de plantillas permite definir una clase genérica que instancie versiones de mVector para matrices de cualquier tipo especificado por un parámetro. La ventaja de este diseño parametrizado, es que cualquiera que sea el tipo de objetos utilizados por las especializaciones de la plantilla, las operaciones básicas son siempre las mismas (inserción, borrado, selección de un elemento, etc).

 

4.1 Definición de una clase genérica

La definición de una clase genérica tiene el siguiente aspecto:

 

template class nombreClase

{

...

};

 

Una clase genérica puede tener una declaración adelantada (forward) para ser declarada después:

 

template class nombreClase;

...

template class nombreClase

{

...

};

 

pero recuerde que debe ser definida antes de su utilización y evidentemente, solo puede definirse una vez .
Observe que la definición de una plantilla comienza siempre con template, y que los parámetros de la lista no son valores, sino tipos de datos . La definición de la clase genérica correspondiente al caso anterior es la siguiente:

 

template class mVector

{

int dimension;

public:

T* mVptr;

mVector(int n = 1)

{

dimension = n;

mVptr = new T[dimension];

}

~mVector() { delete [] mVptr; }

T& operator[](int i) { return mVptr[i]; }

void mostrar (int);

};

template void mVector::mostrar (int i)

{

if((i >= 0) && (i <= dimension)) mVptr[i].mostrar();

}

 

Observe que aparte del cambio de la declaración, se han sustituido las ocurrencias de Vector (un tipo concreto) por el parámetro T. Observe también la definición de mostrar() que se realiza off-line con la sintaxis de una función genérica.
Recordemos que en estas expresiones, el especificador class puede ser sustituido por typename , de forma que la primera línea puede ser sustituida por:

 

template class mVector

{

...

};

 

Veamos el ejemplo completo:

 

#include

class Vector

{

 public:

int x, y;

Vector& operator= (const Vector& v)

{

x = v.x; y = v.y;

return *this;

}

void mostrar(){cout << "X = " << x << "; Y = " << y << endl;}

};

 

template class mVector

{

int dimension;

public:

T* mVptr;

mVector& operator=(const mVector& mv)

{

delete [] mVptr;

dimension = mv.dimension;

mVptr = new T[dimension];

for(int i = 0; i

mVptr[i]= mv.mVptr[i];

return *this;

}

mVector(int n = 1)

{

dimension = n;

mVptr = new T[dimension];

}

~mVector() {delete [] mVptr;}

mVector(const mVector& mv)

{

dimension = mv.dimension;

mVptr = new T[dimension];

for(int i = 0; i

mVptr[i]= mv.mVptr[i];

}

T& operator[](int i) { return mVptr[i]; }

void mostrar (int);

void mostrar ();

};

 

template void mVector::mostrar (int i)

{

if((i >= 0) && (i <= dimension)) mVptr[i].mostrar();

}

 

template void mVector::mostrar ()

{

cout << "Matriz de: " << dimension << " elementos." << endl;

for (int i = 0; i

{

cout << i << "- ";

mVptr[i].mostrar();

}

}

 

void main()

{

mVector mV1(5);

mV1[0].x = 0; mV1[0].y = 1;

mV1[1].x = 2; mV1[1].y = 3;

mV1[2].x = 4; mV1[2].y = 5;

mV1[3].x = 6; mV1[3].y = 7;

mV1[4].x = 8; mV1[4].y = 9;

mV1.mostrar();

mVector mV2 = mV1;

mV2.mostrar();

mV1[0].x = 9; mV1[0].y = 0;

mV2.mostrar(0);

mV1.mostrar(0);

mVector mV3(0);

mV3.mostrar();

mV3 = mV1;

mV3.mostrar();

}

 

Salida:

 

Matriz de: 5 elementos.

0- X = 0; Y = 1

1- X = 2; Y = 3

2- X = 4; Y = 5

3- X = 6; Y = 7

4- X = 8; Y = 9

Matriz de: 5 elementos.

0- X = 0; Y = 1

1- X = 2; Y = 3

2- X = 4; Y = 5

3- X = 6; Y = 7

4- X = 8; Y = 9

X = 0; Y = 1

X = 9; Y = 0

Matriz de: 0 elementos.

Matriz de: 5 elementos.

0- X = 9; Y = 0

1- X = 2; Y = 3

2- X = 4; Y = 5

3- X = 6; Y = 7

4- X = 8; Y = 9

 

Otro ejemplo que describe una clase genérica para la realización de una pila de datos sin que se utilicen listas enlazadas y reserva dinámica de memoria adicional durante el funcionmiento de un objeto de una clase instanciada es el siguiente:

 

// fichero Pila.h

template

// declaración de la clase

class Pila

{

public:

Pila(int nelem=10); // constructor

void Poner(T);

void Imprimir();

private:

int nelementos;

T* cadena;

int limite;

};

 

// definición del constructor

template Pila::Pila(int nelem)

{

nelementos = nelem;

cadena = new T(nelementos);

limite = 0;

};

 

// definición de las funciones miembro

template void Pila::Poner(T elem)

{

if (limite < nelementos)

cadena[limite++] = elem;

};

 

template void Pila::Imprimir()

{

int i;

for (i=0; i

cout << cadena[i] << endl;

};

 

El programa principal puede ser el que sigue:

 

#include

#include "Pila.h"

void main()

{

Pila p1(6);

p1.Poner(2);

p1.Poner(4);

}

 

4.2 Miembros de clases genéricas

Los miembros de las clases genéricas se definen y declaran exactamente igual que los de clases concretas. Pero debemos señalar que las funciones-miembro son a su vez plantillas parametrizadas (funciones genéricas) con los mismos parámetros que la clase genérica a que pertenecen.
Consecuencia de lo anterior es que si las funciones-miembro se definen fuera de la plantilla, sus prototipos deberían presentar el siguiente aspecto:

 

template class mVector

{

int dimension;

public:

T* mVptr;

template mVector& operator=(const mVector&);

template mVector(int);

template ~mVector();

template mVector(const mVector& mv);

T& operator[](int i) { return mVptr[i]; }

template void mostrar (int);

template void mostrar ();

};

 

Sin embargo, no es exactamente así por diversas razones: La primera es que, por ejemplo, se estaría definiendo la plantilla mostrar sin utilizar el parámetro T en su lista de argumentos (lo que en un principio no está permitido ). Otra es que no está permitido declarar los destructores como funciones genéricas. Además los especificadores referidos a mVector dentro de la propia definición son redundantes.
Estas consideraciones hacen que los prototipos puedan ser dejados como sigue (los datos faltantes pueden  ser deducidos por el contexto):

 

template class mVector

{

int dimension;

public:

T* mVptr;

mVector& operator= (const mVector&);

mVector(int); // constructor por defecto

~mVector(); // destructor

mVector(const mVector& mv); // constructor-copia

T& operator[](int i) { return mVptr[i]; }

void mostrar (int); // función auxiliar

void mostrar (); // función auxiliar

};

 

Sin embargo, las definiciones de métodos realizadas off-line (fuera del cuerpo de una plantilla) deben ser declaradas explícitamente como funciones genéricas. Por ejemplo:

 

template void mVector ::showmem (int i)

{

...

}

 

4.3 Miembros estáticos

Las clases genéricas pueden tener miembros estáticos (propiedades y métodos). Posteriormente cada especialización dispondrá de su propio conjunto de estos miembros. Estos miembros estáticos deben ser definidos fuera del cuerpo de la plantilla, exactamente igual que si fuesen miembros estáticos de clases concretas:

 

template class mVector

{

...

static T* mVptr;

static void mostrarNumero (int);

...

};

 

template T* mVector::nVptr;

template void mVector::mostrarNumero(int x){ ... };

 

 

4.4 Métodos genéricos

Hemos señalado que, por su propia naturaleza, los métodos de clases genéricas son a su vez (implícitamente) funciones genéricas con los mismos parámetros que la clase, pero pueden ser además funciones genéricas explícitas (que dependan de parámetros distintos de la plantilla a que pertenecen):

 

template class A

{

// método genérico de clase genérica

template void func(T& t);

...

}

 

Según es usual, la definición del miembro genérico puede efectuarse de dos formas, inline u off-line:

 

#include

template class A

{

public:

int x;

X* xptr;

template void fun(T t1, T t2)

{

// método genérico

cout << "Valor-1: " << t1

<< ", Valor-2: " << t2

<< ", Miembro-x: " << x

<< ", Objeto: " << *xptr << endl;

}

A (X* b, int i = 0)

{

x = i;

xptr = b;

}

};

 

int main(void)

{

char c = 'c'; char* cptr = &c;

int x = 13; int* iptr = &x;

A a(iptr, 2);

A b(cptr, 3);

a.fun(2, 3);

a.fun('x', 'y');

b.fun(2, 3);

b.fun('x', 'y');

return 0;

}

 

Salida:

 

Valor-1: 2, Valor-2: 3, Miembro-x: 2, Objeto: 13

Valor-1: x, Valor-2: y, Miembro-x: 2, Objeto: 13

Valor-1: 2, Valor-2: 3, Miembro-x: 3, Objeto: c

Valor-1: x, Valor-2: y, Miembro-x: 3, Objeto: c

 

4.5 Instanciación de clases genéricas

Las clases genéricas son entes de nivel superior a las clases concretas. Representan para las clases normales (en este contexto preferimos llamarlas clases explícitas) lo mismo que las funciones genéricas a las funciones concretas. Como aquellas, solo tienen existencia en el código. Como el mecanismo de plantillas C++ se resuelve en tiempo de compilación, ni en el fichero ejecutable ni durante la ejecución existe nada parecido a una clase genérica, solo existen especializaciones. En realidad la clase genérica que se ve en el código, actúa como una especie de "macro" que una vez ejecutado su trabajo en la fase de compilación, desaparece de escena.
Observe que, como ha indicado algún autor, el mecanismo de plantillas representa una especie de polimorfismo en tiempo de compilación, similar al que proporciona la herencia de métodos virtuales en tiempo de ejecución.
A diferencia de lo que ocurre con las funciones genéricas, en la instanciación de clases genéricas el compilador no realiza ninguna suposición sobre la naturaleza de los argumentos a utilizar, de modo que se exige que sean declarados siempre de forma explícita. Por ejemplo:

 

mVector mv1;

mVector mv2 = mv1; // Error !!

mVector mv2 = mv1; // Ok.

 

Sin embargo, como veremos a continuación, las clases genéricas pueden tener argumentos por defecto, por lo que en estos casos la declaración puede no ser explícita sino implícita (referida a los valores por defecto de los argumentos). La consecuencia es que en estos casos el compilador tampoco realiza ninguna suposición sobre los argumentos a utilizar. Las clases genéricas pueden ser utilizadas en los mecanismos de herencia. En ese caso, la clase derivada estará parametrizada por los mimmos argumentos que la plantilla de la clase base. Por ejemplo:

 

template class Base { ... };

template class Deriv : public Base {...};

 

Los typedef son muy adecuados para acortar la notación de objetos de clases genéricas cuando se trata de declaraciones muy largas o no interesan los detalles. Por ejemplo :

 

typedef basic_string string;

string st1;

 

Las clases genéricas pueden tener argumentos por defecto, en cuyo caso, el tipo T puede omitirse, pero no los ángulos . Por ejemplo:

 

template class mVector {/* ... */};

...

mVector mv1; // Ok. argumento char explícito

mVector mv2; // Ok. argumento int implícito

 

Cada instancia de una clase genérica es realmente una clase, y sigue las reglas generales de las clases. Dispondrá por tanto de su propia versión de todos los miembros estáticos si los hubiere. Estas clases son denominadas implícitas, para distinguirlas de las definidas "manualmente", que se denominan explícitas. La primera vez que el compilador encuentra una sentencia del tipo mVector crea la función-clase para dicho tipo; es el punto de instanciación. Con objeto de que solo exista una definición de la clase, si existen más ocurrencias de este mismo tipo, las funcionesclase redundantes son eliminadas por el enlazador. Por la razón inversa, si el compilador no encuentra ninguna razón para instanciar una clase (generar la función-clase), esta generación no se producirá y no existirá en el código ninguna instancia de la plantilla. Al igual que ocurre con las funciones genéricas, en las clases genéricas también puede evitarse la generación de versiones implícitas para tipos concretos proporcionando una especialización explícita. Por ejemplo:

 

class mVector { ... }; // definición genérica

...

class mVector { ... }; // definición específica

 

más tarde, las declaraciones del tipo

 

mVector mv1, mv2;

 

generará objetos utilizando la definición específica. En este caso mv1 y mv2 serán matrices alfanuméricas (cadenas de caracteres).

Observe que la definición explícita comporta dos requisitos:

  • Aunque es una versión específica (para un tipo concreto), se utiliza la sintaxis de plantilla: class mVector {...};

  • Se ha sustituido el tipo genérico por un tipo concreto . Resulta evidente que una definición específica, como la incluída, solo tiene sentido si se necesitan algunas modificaciones en el diseño cuando la clase se refiera a tipos char* (punteros-a-char).

 

4.6 Argumentos de la plantilla

La declaración de clases genéricas puede incluir una lista con varios parámetros. Estos pueden ser casi de cualquier tipo: complejos, fundamentales, por ejemplo un int, o incluso otra clase genérica (plantilla). Además en todos los casos pueden presentar valores por defecto.:

 

template class mVector { ... };

 

Al igual que en las funciones genéricas, en la instanciación de clases genéricas, los valores de los parámetros que no sean tipos complejos deben ser constantes o expresiones constantes.

 

const int K = 128;

int i = 256;

mVector mV1; // OK

mVector mV2; // Error: i no es constante

 

Este tipo de parámetros constantes son adecuados para establecer tamaños y límites. Sin embargo, por su propia naturaleza de constantes, cualquier intento posterior de alterar su valor genera un error.

Los argumentos también pueden ser otras plantillas, pero solo de clases genéricas. Introducir como argumento una plantilla de funcione genérica no está permitido:

 

template class C> class MatrizC

{

...

};

template void Func(X a)> class MatrizF

{ // Error!!

...

};

 

Hay que tener en cuenta que no existe algo parecido a un mecanismo de "sobrecarga" de las clases genéricas paralelo al de las funciones genéricas. Por ejemplo las siguientes declaraciones producen un error de compilación.

 

template class mVector { ... };

template class mVector { ... };

 

El compilador daría el siguiente error: Number of template parameters does not match in redeclaration of 'Matriz'.

 

4.7 Punteros y referencias a clases implícitas

Como hemos señalado, las clases implícitas gozan de todas las prerrogativas de las explícitas, incluyendo por supuesto la capacidad de definir punteros y referencias. En este sentido no se diferencian en nada de aquellas; la única precaución es tener presente la cuestión de los tipos a la hora de efectuar asignaciones, y no perder de vista que la plantilla es una abstracción que representa múltiples clases (tipos), cada una representada por argumentos concretos. Consideremos la clase genérica Matriz cuya declaración es:

 

template class Matriz { /* ... */};

 

La definición de punteros y referencias sería como sigue:

 

Matriz m1; // Ok. Tres objetos

Matriz m2; // de tipos

Matriz m3; // distintos.

...

Matriz* ptrMi5 = &m2 // Error!! tipos distintos

Matriz* ptrMch5 = &m2; // Ok.

Matriz* ptrMch1 = &m2; // Error!! tipos distintos

Matriz* ptrMch1 = &m4; // Ok.

ptrMch5->show(); // Ok. invocación de método

Matriz m4 = *ptrMch1; // Ok asignación mediante puntero

void (Matriz::* fptr1)(); // Ok. declara puntero a método

fptr1 = &Matriz::show; // Ok. asignación

(m3.*fptr1)(); // Error!! tipo de m3 incorrecto

(m2.*fptr1)(); // Ok. invocación de método

Matriz& refMch5 = m2; // Ok. referencia

Matriz& refMch1 = m4; // Ok.

refMch5.show(); // Ok. invocación de método

 

Merecen especial atención las sentencias donde se utilizan punteros a miembros de clases implícitas. El tipo de clase está definido en los parámetros. Observe que fptr1 es puntero a método de clase Matriz, y no puede referenciar un método de m3, que es un objeto de tipo Matriz.

 

p1.Imprimir();

Pila p2(6);

p2.Poner('a');

p2.Poner('b');

p2.Imprimir();

}

 

 

5 Clases genéricas en la Librería Estándar

Al principio del capitulo se ha señalado que las plantillas fueron introducidas en C++ para dar soporte a determinadas técnicas utilizadas en la Librería Estándar; de hecho, la STL está constituida casi exclusivamente por plantillas. Hablar y utilizar en profundidad las plantillas de la librería estándar puede permitirnos el reducir drásticamente el esfuerzo necesario para la programación, son embargo comprenderlas e introducirse en el modo en que se han programado requiere ya de una soltura en el manejo del lenguaje que queda fuera de los objetivos de este curso. A continuación se relacionan algunas de las plantillas más importantes y más comunes.:

 

- basic_string: una plantilla para utilizar entidades como secuencias de caracteres. Está definida en el fichero de cabecera , y responde a la siguiente declaración:

 

template ,

class Allocator = allocator > class basic_string;

 

Como puede verse, acepta tres argumentos, de los que dos tienen valores por defecto. Existen dos especializaciones que tienen nombres específicos. En concreto si charT es char la especialización se denomina string, y wstring si charT es wchar_t. Esto se hace mediante sendos typedef:

 

typedef basic_string string;

typedef basic_string wstring;

 

de forma que las dos sentencias siguientes son equivalentes:

 

basic_string s1;

string s1;

 

- vector. Un vector es la versión STL de una matriz dinámica de una dimensión. Las instancias de esta clase genérica conforman una secuencia (una clase de contenedor ). La clase dispone de acceso aleatorio a sus elementos y de un mecanismo muy eficiente de inserción y borrado de elementos al final. Aunque también pueden insertarse y borrarse elementos en medio. Está definida en el fichero y responde a la siguiente declaración:

 

template > class vector;

 

Una especialización concreta de esta plantilla, cuando el tipo T es un booleano vector, es considerada por la STL como un caso especial, con un código optimizado que incluye métodos no disponibles para el resto de las instancias (los métodos flip y swap) pensado para trabajar con representaciones bit a bit.

 

- list. Las instancias de esta plantilla conforman también una secuencia que dispone de mecanismos muy eficientes para insertar y eliminar elementos en cualquier punto. Está definida en el fichero y responde a la siguiente declaración:

 

template > class list;

 

- map. Esta plantilla está definida en el fichero , y tiene la siguiente declaración:

 

template

class Allocator = allocator> > class map;

 

Las instancias de esta plantilla conforman un contenedor asociativo (una clase de contenedor) que permite almacenar y acceder objetos de tipo T indexados mediante una única clave Key, disponiendo de mecanismos muy eficientes de inserción y borrado. 

 

- auto_ptr. Esta plantilla genera punteros "inteligentes" que destruyen automáticamente el objeto señalado cuando ellos mismos salen de ámbito. Su definición está en el fichero , y su declaración es:

 

template class auto_ptr;

 

En realidad esta plantilla se ha incluido en la Librería Estándar para ayudar a resolver el problema de los objetos persistentes, creados con el operador new, cuyo referente (el puntero que los señala) es un objeto automático que puede ser destruido inadvertidamente al salir de ámbito. Lo que puede ocurrir en el los procesos de lanzamiento y captura de excepciones.

 

 

6 Ejemplo

El siguiente estracto de código muestra una clase genérica que sirve para alamecenar punteros de objetos o elementos creados externamente a ella. Internamente se comporta como un vector dinámico que admite por tanto un número indefinido de elementos. Incluye además una serie de operaciones básicas para facilitar el manejo y mantenimiento del conjunto de direcciones almacenadas:

 

Fichero contenedor.h

template class contenedor

{

private:

T **array;

int nElem;

int elemMax;

public:

contenedor(void);

contenedor(contenedor &td); /*copia*/

~contenedor(void);

inline T* operator [] (int a);

void operator += (T *ele);

void destruirObjetos(void);

void eliminar (int a);

T* quitar(int a);

T* quitar(T* ele);

int buscar(T* ele);

void swap(int a, int b);

void insertar(T *ele, int a);

inline int numElem (void);

};

 

Fichero contenedor.cpp

#include “contenedor.h”

template contenedor::contenedor (void)

{

array = new T* [5];

nElem = 0;

elemMax = 5;

}

template void contenedor::destruirObjetos (void)

{

for (int i=0; i

delete array[i];

numElem=0;

}

template contenedor::~contenedor (void)

{

delete [] array;

}

template contenedor::contenedor (contenedor & td)

{

int i;

nElem = td.nElem;

elemMax = td.elemMax;

array = new T* [elemMax];

for(i=0;i

}

template T* contenedor::operator [] (int a)

{

if ((a>=0)&&(nElem>a))return array[a];

return NULL;

}

template void contenedor::operator += (T* ele)

{

if (nElem == elemMax)

{

elemMax+=5;

T** array2 = new T* [elemMax];

memcpy (array2,array,nElem*sizeof(T*));

delete [] array;

array=array2;

}

array[nElem] = ele;

nElem ++;

}

template T* contenedor::quitar(int a)

{

T* aux;

if((a>=0)&&(a

{

aux=array[a];

array[a]=NULL;

for(int i=a+1;i

array[i-1]=array[i];

return aux;

}

return NULL;

}

template T* contenedor::quitar(T* ele)

{

return (quitar(buscar(ele)));

}

template int contenedor::buscar(T* ele)

{

for(int i=0;i

return -1;

}

template void contenedor::swap(int a,int b)

{

T* aux;

if((a>=0)&&(a=0)&&(b

}

template void contenedor::insertar(T *ele, int a)

{

if(a<0)return;

(*this)+=ele;

swap(nElem-1,a);

}

template int contenedor::numElem (void)

{

return nElem;

}

 

 



 


Banner
Spanish Chinese (Simplified) English French German Japanese Portuguese Russian