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.1a  Instanciación explícita  &  versión explícita  (sobrecarga de funciones genéricas)

§1  Sinopsis

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 ( 4.12.1), 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:

  • a.  Proporcionando una versión codificada de forma "manual" de la función (versión explícita ).
  • b.  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:

    • b1  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 .
    • b2  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 ( 4.4.1a) que incluya funciones explícitas y genéricas.

Además de las anteriores existe una cuarta posibilidad, que en realidad podría considerarse una variación del caso b1 anterior (la hemos visto en el capítulo anterior bajo el epígrafe "Especificación explícita de parámetros" ( 4.12.1). Tiene la particularidad de que la instanciación no se realiza independientemente de la utilización posterior del código, sino en el momento de su uso (cuando existe una invocación de la función). A esta modalidad la denominamos instanciación implícita específica, y resulta de utilidad cuando a la luz de los argumentos utilizados en la invocación, puede existir ambigüedad sobre los parámetros que debe utilizar el compilador en la plantilla. Su característica principal es que no se permite al compilador decidir por su cuenta que parámetros utilizará en la plantilla en función de los argumentos de la invocación, sino que se le ordena generar una plantilla utilizando unos parámetros determinados (de ahí el nombre que utilizamos para ella) y que aplique los argumentos actuales en la invocación del código resultante.

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<class T> T max(T, T);

...

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

template<class T> 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>(long a, long b);

...

// instanciación explícita particular

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

...

// instanciación implícita específica

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

§1.1  Introducción

Para introducirnos en el tema , considere un caso en el que utilizamos una función genérica igual( ) para comprobar si dos objetos son iguales:

#include <iostream>
using namespace std;

class Vector {
  public: float x, y;
  bool operator==(const Vector& v) {        // L6
    return ( x == v.x && y == v.y)? true : false;
  }
};
template<class T> bool igual(T a, T b) {    // L10: 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;  // M7
  else cout << "doubles distintos" << endl;
  if ( igual(x, y) ) cout << "enteros iguales" << endl;
  else cout << "enteros distintos" << endl;
}

Salida:

vectores distintos
doubles distintos
enteros distintos

Comentario

En L6 se ha definido una versión sobrecargada del operador de igualdad == para los miembros de la clase. En L10 se define la función genérica igual(T, T).

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

§2  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 <iostream>
using namespace std;

class Vector {
  public: float x, y;
  bool operator==(const Vector& v) {
    return ( x == v.x && y == v.y)? true : false;
  }
};
template<class T> bool igual(T a, T b) {   // L10: función genérica

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

};
bool igual(double a, double b) {           // L13: 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;  // M7b
  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

Comentario

La versión explícita para tipos double de L13 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 M7 del primer caso (§1.1 ):

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

por:

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

Se obtienen un error de compilación: Could not find a match for 'igual<T>(double,int)'. La razón es que, como hemos visto ( 4.12.1), 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 M7 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 sí es capaz de realizar automáticamente determinadas transformaciones de los argumentos actuales para adecuarlos a los esperados por la función ( 4.4.6).

§3  Instanciación explícita

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<class T> bool igual(T a, T b) {...}     // función genérica

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

template bool igual<double>(double a, double b);  // instancia explicita  §3a

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

Observe que la versión instanciada en la expresión §3a 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, también es posible 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>(double a, double b) {  // instancia explicita particular
   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 (§2 ).

Nota: al llegar a este punto el lector puede, con razón, suscitarse la cuestión ¿Que diferencia existe entonces entre una versión explícita y una instanciación explícita particular?.


§3.1  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) {                 // versión explícita
   return (labs(a-b) < 1.0) ? true : false;
};
template bool igual<double>(double& a, double& b);  // instancia explicita general

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.

En la página adjunta se incluye un ejemplo ejecutable ( Ejemplo). Observe que la instanciación explícita no es realmente necesaria desde el punto de vista del programa (a no ser que se busque un efecto de conversión automática de tipos como el descrito). El Dr. Stroustrup aclara que este mecanismo solo es de utilidad para la depuración, optimización, y control de los procesos de compilación y enlazado.

§4  Conversión de argumentos en versiones explícitas e implícitas

Recuerde que cuando se siguen los pasos de la congruencia estándar de argumentos para resolver la sobrecarga de funciones ( 4.4.1a), el compilador puede realizar determinadas conversiones para realizar la invocación.

§4.1  Considere el siguiente ejemplo con una función explícita:

#include <iostream>
using namespace std;

int max(int a, int b) { return (a > b) ? a : b; }  // L4

void main() {         // =====================
  int x = 2, y = 3;
  char c = 'x';
  cout << "Mayor: " << max(x, y) << endl;    // M3
  cout << "Mayor: " << max(x, c) << endl;    // M4
}

Salida:

Mayor: 3
Mayor: 120

Comentario

La invocación en M3 de la función max( ) no presenta problemas. Pero en M4, para conseguir el ajuste con la mejor (y única) versión de la función, el compilador debe realizar una promoción de char a int. Lo que se realiza sin problema según puede verse en la segunda salida, donde ha sustituido el char 'x' por su valor ASCII (ver conversiones aritméticas 2.2.5).


§4.2  Considere ahora una nueva versión, en la que las invocaciones a la versión explícita de max (en M3 y M4) son sustituidas por una versión implícita, generada en cada caso por una función genérica:

#include <iostream>
using namespace std;

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

void main() {         // =====================
  int x = 2, y = 3;
  char c = 'x';
  cout << "Mayor: " << max(x, y) << endl;    // M3
  cout << "Mayor: " << max(x, c) << endl;    // M4
}


En este caso se recibe un error de compilación en M4:  Template parameter 'T' is ambiguous could be 'char' or 'int'. La razón es que cuando se trata de definiciones generadas automáticamente por el compilador (versiones implícitas), solo se realizan conversiones triviales de argumentos . Así pues, no se realiza la conversión char a int o viceversa.

Para resolver la ambigüedad pueden utilizarse dos formas:

a.-  Proporcionarse una versión explícita.  Por ejemplo, añadir la siguiente línea al código anterior [2]:

int max(int a, char b) { return (a > b) ? a : b; }   // L5

b.-  Incluir una cualificación explícita en la invocación.  Por ejemplo, sustituir la sentencia M4 por:

cout << "Mayor: " << max<int>(x, c) << endl;    // M4bis

Corolario:

Estas consideraciones sobre los modos de conversión de argumentos, puede tener en la práctica importantes consecuencias. Empezando por que, como hemos visto, el hecho de que una función explícita se comporte bien en un programa no significa que lo siga haciendo si se generaliza y se transforma en una plantilla.

La cuestión no solo alcanza a las funciones genéricas; como veremos a continuación ( 4.12.2), las funciones-miembro de clases genéricas son a su vez funciones genéricas con los mismos parámetros que la clase genérica a que pertenecen, con lo que el problema descrito puede reproducirse también en las clases (volveremos sobre este particular al tratar el capítulo correspondiente).

  Inicio.


[1]  También podría ser considerada una instanciación implícita en la que se especifica explícitamente al compilador los argumentos a utilizar con la plantilla.

[2] A este respecto el comportamiento de MS Visual C++ 6.0 (el más antiguo de los analizados) muestra un comportamiento deficiente. La plantilla parece tener precedencia sobre la versión explícita de la función.