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.1  Funciones genéricas

§1  Sinopsis

Hemos indicado ( 1.12) que las plantillas-función o funciones genéricas son un mecanismo C++ que permite definir una función mediante uno o varios parámetros. A partir de estas plantillas, el compilador es capaz de generar código de funciones distintas que comparten ciertas características. Las funciones así generadas se denominan instancias o especializaciones de la plantilla. También versiones implícitas, para distinguirlas de las versiones codificadas manualmente (versiones explícitas). Este mecanismo resulta esencial para la creación de algoritmos genéricos como los utilizados en la STL ( 5.1), donde las funciones genéricas son utilizadas extensivamente como miembros de clases (en especial como constructores parametrizados).

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 ( 2.2), 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 una variable. Una posible solución es sobrecargar la función max(), definiendo tantas versiones como tipos distintos debamos utilizar.

Otra alternativa sería utilizar una macro:

#define max(a, b) ((a > b) ? a : b)

pero hemos señalado que esto presenta sus inconvenientes ( 4.9.10b). 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 [1].

§2 Funciones genéricas

La solución al problema enunciado es utilizar una función genérica (plantilla). La sintaxis de su definición es la siguiente:

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

  template es un especificador de tipo, e indica que se trata de una plantilla (es una palabra clave C++)

  <class T> es la lista de parámetros; representa el/los parámetros de-la-plantilla (el tipo de dato ). 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).

Los parámetros de la plantilla pueden ser tipos concretos, valores constantes o plantillas (ver la gramática ). Lo mismo que en las funciones explícitas, las genéricas pueden ser declaradas antes de su utilización:

template <class T> T max(T, T);

y definidas después:

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

ver a este respecto "Las plantillas y la organización del código" ( 4.12.1b). En la página adjunta se muestra la gramática C++ para este especificador ( Gramática). Recuerde que la definición de una plantilla comienza siempre con template <...>

La idéa 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 [1a]. Por ejemplo, la plantilla anterior puede ser utilizada mediante las siguientes sentencias:

int i, j;
UnaClase a, b;
...
int k = max(i, j);              // L1
UnaClase c = max(a, b);         // L2


En L1 los argumentos de-la-función son dos objetos tipo int; mientras en L2 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. Veremos más adelante , que 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). Veremos también las medidas sintácticas adoptadas cuando tal deducción no es posible.

  Observe que en este tipo de funciones, los argumentos pueden ser objetos muy complejos cuya construcción puede ser costosa, razón por la que es preferible pasar los argumentos por referencia antes que por valor ( 4.4.5). Así, la declaración de la función anterior estaría mejor en la forma:

template <class T> T max(T&, T&);


§2.1  Una función genérica puede tener más argumentos que la plantilla. Por ejemplo:

template <class T> void func(T, inf, char, long, ...);

También puede tener menos:

template <class T> void func();

Nota: mas adelante se muestra la forma de operar en este caso, para que el compilador deduzca el parámetro correcto T a utilizar en la plantilla .


§2.2 La gramática muestra que el especificador class de la lista de parámetros puede ser sustituido por typename ( 3.2.1e), de forma que la expresión anterior equivale a:

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

§3  Observaciones

Llegados a este punto algunas observaciones importantes:


§3.1  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 objetos; en cómo se ha definido el operador > para ese tipo concreto. Esta es justamente la premisa fundamental de la programación genérica ( 4.12)

§3.2 Instanciación

Los parámetros de la plantilla introducen ciertos identificadores (nombres como el T de los ejemplos anteriores) en el ámbito de la definición de esta, que pueden referirse a tipos; constantes u otras plantillas (a su vez, los tipos pueden ser básicos; extendidos o compuestos  2.2a).  Cuando la plantilla es instanciada, los identificadores son sustituidos por tipos o constantes concretas y el código es compilado (cuando los parámetros son a su vez plantillas, los identificadores involucrados son sustituidos en un proceso recursivo hasta que todos han sido sustituidos por tipos o constantes concretos).

La instanciación de la plantilla se produce cuando el compilador necesita una versión concreta (especialidad) de la función genérica, lo que sucede cuando encuentra una invocación como L.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 argumentos actuales [3]. Ejemplo:

int m, m;
UnaClase a, b;
...
int j = max(m, n);             // L1: versión para enteros
UnaClase obj = max(a, b);      // L2: versión para objetos UnaClase


En este caso, al llegar a L1 el compilador genera el código de una función que acepta dos int, e incluye en este punto una invocación a la dirección correspondiente con los parámetros adecuados. En L2 las cosas ocurren de forma análoga. El compilador genera el código para una función que acepte dos objetos tipo UnaClase, e inserta el código para la invocación pertinente.


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 [6].

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 ( 5.1), 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).

La unificación de las definiciones que aparezcan en más de un módulo, depende naturalmente de la capacidad de optimización del compilador, pero en general, el problema de las funciones genéricas es el "code bloat". La contrapartida es que resultan funciones rápidas (tanto como la versión explícita equivalente).

Debido a que las funciones genéricas permiten código fuente genérico, en cierta forma han sido comparadas con las funciones virtuales ( 4.11.8a), dado que el polimorfismo permite escribir código objeto genérico [5]. En este sentido, las funciones virtuales representarían una alternativa a las funciones genéricas. La ventaja en este caso sería la mayor compacidad del código. La contrapartida es que el "Late bindign" de las funciones virtuales las hace comparativamente más lentas.


Suponiendo para la plantilla max(a, b) la definición anterior (§2 ), el compilador se preocupa de sustituir la expresión a > b por la apropiada invocación de la función a.operator>(b) ( 4.9.18). Lo que significa que la función puede emplearse con cualquier tipo para el que se haya definido la función-operador correspondiente ( 4.9.18b1).

Puesto que los tipos básicos disponen de sus propias versiones (globales) de las funciones-operador ( 4.9.18), una plantilla como §2, puede ser utilizada no solo con tipos complejos, también con los tipos fundamentales.

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 ( 4.12.1a).

§4 Ejemplo 

Veamos un caso concreto con una función genérica que utiliza la clase Vector a la que ya nos hemos referido en capítulos anteriores [2]:

#include <iostream>
using namespace std;

class Vector {
  public: float x, y;
  bool operator>(Vector v) {    // L6 operador > para la clase
    return ((x * x + y * y) > (v.x * v.x + v.y * v.y))? true: false;
  }
};
template<class T> T max(T a, T b) { return (a > b) ? a : b; };   // L10

void main() {                   // =====================
  Vector v1 = {2, 3}, v2 = {1, 5};
  int x = 2, y = 3;
  cout << "Mayor: " << max(x, y) << endl;       // M3
  cout << "Mayor: " << max(v1, v2).x << ", " << max(v1,v2).y << endl; // M4
}

Salida:

Mayor: 3
Mayor: 1, 5

Comentario

En L6 se ha definido una versión sobrecargada del operador binario > para los miembros de la clase. En L10 se define la función genérica (plantilla)

T max(T, T).

Puede comprobarse que el compilador ha generado, y utilizado, correctamente el código de las funciones max(int, int) para la invocación de M3, y max(Vector, Vector) para M4.

Observe que la utilización de la función genérica max con objetos de cualquier clase C, exige que en la definición de la clase esté sobrecargado el operador >. Es decir, debe existir una función-operador del siguiente aspecto:

bool operator>(C c) {     // operador > para la clase
   ...;
}

§5 Métodos genéricos

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

class A {
  template<class T> void func(T& t) { // definición de método genérico
    ...
  }
  ...
}

La definición de métodos genéricos puede hacerse también fuera del cuerpo de la clase (off-line):

class A {
  template<class T> void func(T& t); // declaración de método genérico
  ...
}
...
template <class T> void A::func(T& t) {  // definición off-line
  ...
}

Ver ejemplo ejecutable de un método genérico en una clase ( Ejemplo)


Los miembros genéricos pueden ser a su vez miembros de clases genéricas ( 4.12.2):

template<class X> class A {          // clase genérica
  template<class T> void func(T& t); // método genérico de clase genérica
  ...
}

§6 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 ( 2.2), por ejemplo un int; incluso especializaciones de clases genéricas ( 4.12.2) y constantes de tiempo de compilación. Por contra, las funciones genéricas no pueden ser argumentos.

template <class A, class B> void func(A, B);
tamplate <class A, func(A, B)> void f(A, B);   // Error!!
template <class A, int x> void func(A, int);
template <class A, size_t N> void func(A, N);
template<class T> class X {...};               // una clase genérica
template<class A, class T> void func(A, X<T>);


§6.1   A diferencia de las clases genéricas, los argumentos 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 <class A, int x> void func(A, int);
...
func(T);       // Error!! falta 1 argumento

§6.2 Evitar indefiniciones

Todos los argumentos formales de-la-plantilla (contenidos en la lista template <....> ) deben 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 <....> cuando se produzca una invocación específica de la función.

Ejemplo:

template <class A, class B> 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 de-la-plantilla. Por ejemplo, en MS VC++ es el error C2783: 'declaration': could not deduce template argument for 'identifier'.

§6.3 Especificación explícita de parámetros

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:

templete <class T> T* build() {   // función genérica
  try { return T* = new T; }
  catch (bad_alloc) {
    cout << "Memoria agotada" << endl;
    abort();
  }
}

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 memoria suficiente, pero el compilador no puede deducir el tipo de parámetro a utilizar con la plantilla a partir del argumento actual:

int* iptr = build();      // Error parámetro T desconocido

Para su utilización debe especificarse explícitamente el tipo de parámetro a utilizar mediante un argumento:

int* iptr = build<int>(); // Ok. T es int


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: foo <...> (...).

Esta forma se denomina instanciación implícita específica [4], 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 4.4.5).

Ejemplo:

template <class A, class B, class C> void func(B b, C c, int i);
....
func(b, c, i);           // Error!!
func <A, B, C>(b, c, i); // Ok. B y C redundantes
func<A>(b, c, i);        // Ok.
func<A>(b, c);           // Error!! falta argumento i


En ocasiones los parámetros de plantilla ayudan a determinar el tipo de instanciación a utilizar en cada caso.  Por ejemplo, hemos visto que la función genérica

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

Permite utilizarla en invocaciones del tipo:

cout << max(5, 10) << endl;       // -> 10
cout << max('A', 'B') << endl;    // -> B

Pero si queremos utilizar distintos tipos en una misma invocación necesitamos los parámetros de plantilla para instruir al compilador sobre el tipo a utilizar [7]:

cout << max<int>(5, 'B') << endl;     // -> 66
cout << max<char>(5, 'B') << endl;    // -> B


La Librería Estándar ofrece algunos ejemplos de funciones genéricas que precisan de instanciación implícita específica. Por ejemplo las funciones has_facet( 5.2.1) y use_facet( 5.2.1)

Nota: la posibilidad de especificar explícitamente los parámetros, permite implementar un pseudo-operador

implicit_cast<T>(arg)

que puede funcionar de forma semejante a los operadores C++ de modelado ( implicit_cast)

§6.4 Evitar ambigüedades

Como señalábamos al principio, 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 (argumentos actuales 4.4.5).

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 ( 4.4.1a).

En caso de funciones genéricas, el compilador deduce los parámetros de-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).

Ejemplo:

template<class T> bool igual(T a, T b){  // función genérica
  return (a == b) ? true : false;
}
bool desigual(double a, double b){       // función explícita
  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


§6.4.1
  Lo anterior no es válido para funciones-miembro de clases genéricas ( 4.12.2) ya que estas pueden deducir la identidad de sus miembros por el contexto. Por ejemplo:

template <class A, class B> class ClasA {   // clase genérica
  a...; b...;
  void func();        // función-miembro
};
...
template <class A, class B> void ClasA<A, B>::func() { // definición off-line
  a...; b...;
}


§6.5 Deben evitarse ambigüedades que dificulten al compilador la identificación de los tipos a utilizar en la plantilla. Por ejemplo:

template<class T> class Cx {...};      // clase genérica
template<class T> void func(Cx<T>);    // función genérica
...
func(1);                                // Error!! invocación

En estas circunstancias el compilador produce un error, ya que es incapaz de deducir el argumento T de la plantilla a partir del argumento actual utilizado en la invocación de la función. En cambio, el siguiente código resuelve la ambigüedad:

Cx<int> x = 1;
func(x);      // Ok. x es tipo Cx<int> T es int

Nota: en el capítulo dedicado a las clases genéricas se incluyen más detalles sobre los argumentos del especificador template ( 4.12.2).

  Inicio.


[1]  Otro problema de la utilización de macros es su actuación indiscriminada; realizan la sustitución en cualquier sitio donde se encuentre el macro_identificador. En cambio, la utilización de una plantilla permite establecer un patrón para una familia de funciones sobrecargadas, permitiendo que el tipo de dato a emplear funcione como parámetro.

[1a]  Para evitar confusiones utilizaremos la designación "parámetros" para la plantilla, y "argumentos" (actuales o formales) para la función.

[2] El ejemplo que sigue, se compila correctamente con las opciones por defecto de MS Visual C++ 6.0. En cambio produce errores con Borland C++ 5.5 utilizando igualmente las opciones por defecto ( Comentario).

[3] Quizás necesite ajustar determinadas opciones del compilador ( 1.4.3) para que pueda generarse el código adecuado. Ver en la página adjunta algunas instrucciones al respecto para el compilador Borland ( BC++ 55 compiler switches).

[4] También especificación explícita de argumentos de la plantailla. En el capítulo siguiente se contempla este uso dentro del esquema global de utilización de las funciones genéricas ( 4.12.1a).

[5] "Template Classes for the iostreams Library" de Randal Kamradt en Cpp Users Journal. Enero 1993. En el citado artículo, el autor argumenta que, en ocasiones, la mejor solución puede consistir en una mixtura de ambas técnicas.

[6]  Ver en la página adjunta algunos comentarios al respecto   http://gcc.gnu.org/

[7]  Observe que la esta utilización supone la existencia de un convertidor de tipo por el que el 5 puede ser convertido a char y la 'B' a int.