Documenti di Didattica
Documenti di Professioni
Documenti di Cultura
Como veremos a continuacin, estas posibilidades tienen distinta utilidad y ligeras diferencias de
detalle. Aunque son tcnicas diferentes, el resultado final es anlogo: la existencia de una (o
varias) especializaciones concretas de la funcin, lo que nos obligar a contemplar una
generalizacin de la sobrecarga de funciones (
4.4.1a) que incluya funciones explcitas y
genricas.
Adems de las anteriores existe una cuarta posibilidad, que en realidad podra considerarse una
variacin del caso b1 anterior (la hemos visto en el captulo anterior bajo el epgrafe
"Especificacin explcita de parmetros" (
4.12.1). Tiene la particularidad de que la instanciacin
no se realiza independientemente de la utilizacin posterior del cdigo, sino en el momento de su
uso (cuando existe una invocacin de la funcin). A esta modalidad la denominamos instanciacin
implcita especfica, y resulta de utilidad cuando a la luz de los argumentos utilizados en la
invocacin, puede existir ambigedad sobre los parmetros que debe utilizar el compilador en la
plantilla. Su caracterstica principal es que no se permite al compilador decidir por su cuenta que
parmetros utilizar en la plantilla en funcin de los argumentos de la invocacin, sino que se le
ordena generar una plantilla utilizando unos parmetros determinados (de ah el nombre que
utilizamos para ella) y que aplique los argumentos actuales en la invocacin del cdigo resultante.
Con independencia de la explicacin ms detallada que sigue, para situarnos en el tema
adelantamos un esbozo de lo que significa la literatura anterior referida a un caso muy sencillo:
// funcin genrica (declaracin)
template<class T> T max(T, T);
...
// funcin genrica (definicin)
template<class T> T max(T a, T b) { return (a > b) ? a : b; }
...
// versin explcita
char max(char a, char b) { return (a >= b) ? a : b; }
...
// instanciacin explcita general
template T max<long>(long a, long b);
...
// instanciacin explcita particular
template<> T max<double>(double a, double b){ return (a >= b) ? a : b; };
...
// instanciacin implcita especfica
int x = max<int>(x, 'c');
1.1 Introduccin
Para introducirnos en el tema , considere un caso en el que utilizamos una funcin
genrica 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: funcin genrica
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 (
else
if (
else
if (
else
}
Salida:
vectores distintos
doubles distintos
enteros distintos
Comentario
En L6 se ha definido una versin sobrecargada del operador de igualdad == para los miembros de
la clase. En L10 se define la funcin genricaigual(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 Versin explcita
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 condicin 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: funcin genrica
return (a == b) ? true : false;
};
bool igual(double a, double b) {
void main() {
// =====================
}
Salida:
vectores distintos
doubles iguales
enteros distintos
Comentario
La versin explcita para tipos double de L13 utiliza la funcin de librera labs para conseguir
que dos doubles sean considerados iguales si la diferencia es solo en los decimales. La inclusin
de esta definicin supone que el compilador no necesita generar una versin de igual( ) cuando los
parmetros son tipo double. En este caso, el compilador utiliza la versin suministrada
"manualmente" por el programador.
Adems de permitir introducir modificaciones puntuales en el comportamiento general, las
versiones explcitas pueden utilizarse tambin para eliminar algunas de las limitaciones de las
funciones genricas. 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
3 Instanciacin explcita
El Estndar ha previsto un procedimiento para obligar al compilador a generar el cdigo de una
especializacin concreta a partir de la plantilla-funcin. Esta instanciacin forzada se
denomina instanciacin explcita, y utiliza el especificador template aislado (sin estar seguido
de <...> ).
// funcin genrica
La sintaxis para generar una versin de igual especfica para doubles sera la siguiente:
template bool igual<double>(double a, double b);
explicita 3a
// instancia
// instancia
Los ngulos <> despus de template indican al compilador que sigue una especializacin
particular de una plantilla definida previamente. Como puede figurarse el lector, el resultado es
similar al que se obtendra una versin explcita (2
).
Nota: al llegar a este punto el lector puede, con razn, suscitarse la cuestin Que diferencia
existe entonces entre una versin explcita y una instanciacin explcita particular?.
3.1 Es un error intentar la existencia de ms de una definicin para la misma funcin, ya sea esta
una instanciacin implcita; explcita, o una versin codificada manualmente. Por ejemplo:
bool igual(double& a, double& b) {
// versin explcita
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 versin explcita de
la funcin con idntica firma.
En cualquier caso, es una regla que el compilador dar
preferencia a una funcin normal (versin explcita) sobre cualquier forma de instanciacin,
explcita o implcita, al utilizar una funcin.
// 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 invocacin en M3 de la funcin max( ) no presenta problemas. Pero en M4, para conseguir el
ajuste con la mejor (y nica) versin de la funcin, el compilador debe realizar una promocin
de char a int. Lo que se realiza sin problema segn puede verse en la segunda salida, donde ha
sustituido el char 'x' por su valor ASCII (ver conversiones aritmticas
2.2.5).
4.2 Considere ahora una nueva versin, en la que las invocaciones a la versin explcita
de max (en M3 y M4) son sustituidas por una versin implcita, generada en cada caso por una
funcin genrica:
#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
// M4
// L5
b.- Incluir una cualificacin explcita en la invocacin. Por ejemplo, sustituir la sentencia M4 por:
cout << "Mayor: " << max<int>(x, c) << endl;
// M4bis
Corolario:
Estas consideraciones sobre los modos de conversin de argumentos, puede tener en la prctica
importantes consecuencias. Empezando por que, como hemos visto, el hecho de que una funcin
explcita se comporte bien en un programa
no significa que lo siga haciendo si se generaliza y
se transforma en una plantilla.
La cuestin no solo alcanza a las funciones genricas; como veremos a continuacin (
4.12.2),
las funciones-miembro de clases genricas son a su vez funciones genricas con los mismos
parmetros que la clase genrica a que pertenecen, con lo que el problema descrito puede
reproducirse tambin en las clases (volveremos sobre este particular al tratar el captulo
correspondiente).
// cabecera.h
// main.cpp
#include <cabecera.h>
#ifndef F_1
...
#define F_1
int main() {
...
f1(a);
...
// =====
#endif
// F_1
// modulo1.cpp
// modulo2.cpp
#include <cabecera.h>
#include <cabecera.h>
...
...
{
...
...
f1(X);
f1(Z);
...
...
De esta forma se garantiza que la plantilla pueda ser utilizada por ms de una unidad de
compilacin, y se delega en el compilador la tarea de generar el cdigo adecuado y eliminar las
definiciones redundantes. Observe que es justamente la misma estrategia que con las funciones
explcitas definidas por el usuario ( 4.4.1). El inconveniente es que la declaracin est presente
en todos los mdulos, con la consiguiente sobrecarga para el compilador que debe procesarla.
2.3 El mtodo de compilacin separada consiste en incluir una declaracin en cada mdulo
antes de su uso, e incluir la definicin en un fichero que es compilado separadamente. En este
caso, la disposicin de ficheros queda como sigue:
// cabecera.h
// definiciones.cpp
#include <cabecera.h>
#ifndef F_1
#define F_1
...
// definicin
}
...
// otras declaraciones
...
#endif
// otras definiciones
// F_1
// modulo1.cpp
// modulo2.cpp
#include <cabecera.h>
#include <cabecera.h>
...
...
{
...
f1(X);
...
// usar funcin
f1(Z);
...
}
// usar funcin
...
}
El fichero cabecera.h solo contiene las declaraciones de las plantillas (eventualmente cualquier
otra cabecera necesaria para el proyecto).
El fichero definiciones.cpp, que contiene las definiciones de las plantillas (puede incluir tambin
las definiciones de otras funciones del proyecto), es compilado en una unidad de compilacin
independiente del resto.
El resto de ficheros fuente: modulo-1.cpp; modulo2.cpp; modulo-3.cpp, etc. contienen
un #include al fichero de declaraciones. En este caso, el compilador no tiene que descartar las
copias redundantes de la definicin como en el caso anterior, pero a cambio debe localizar la nica
instancia de la definicin cada vez que sea necesaria. Observe que se necesita la
declaracin export para que la definicin sea accesible desde las otras unidades de compilacin.
3 export es una palabra clave [1] opcional que indica al compilador que la declaracin ser
accesible desde otras unidades de compilacin, y solo puede ser utilizada una vez con cada
declaracin (Recuerde el principio de una sola definicin
4.1.2).
Nota: en la pgina adjunta se incluyen algunas instrucciones para el compilador Borland (
BC++ 55 compiler switches).
// modulo1.cpp
// modulo2.cpp
#include <cabecera.h>
#include <cabecera.h>
...
...
{
class X { int data; };
...
...
f1(X);
// usar funcin
f1(X);
...
}
// usar funcin
...
}
En estos casos pueden ocurrir errores serios, dado que el tipo de X en el primer mdulo no
coincide con el tipo de X en el segundo. Sin embargo, la comprobacin nombre-tipo los hace
parecer como iguales. El corolario es que debe intentarse que la instanciacin implcita de
funciones (o clases) genricas no dependa de tipos locales.
destructor
operador subndice
funcin auxiliar
2 Definicin
La definicin de una clase genrica tiene el siguiente aspecto:
template<lista-de-parametros> class nombreClase {
...
};
// Definicin
Una clase genrica puede tener una declaracin adelantada para ser declarada despus:
template<lista-de-parametros> class nombreClase;
...
template<lista-de-parametros> class nombreClase {
...
};
// Declaracin
// Definicin
pero recuerde que debe ser definida antes de su utilizacin [5], y la regla de una sola definicin (
4.1.2).
Observe que la definicin de una plantilla comienza siempre con template <...>, y que los
parmetros de la lista <...> no son valores, sino tipos de datos
. En la pgina adjunta se muestra
la gramtica C++ para el especificador template (
Gramtica).
La definicin de la clase genrica correspondiente al caso anterior es la siguiente:
template<class T> class mVector {
// L1 Declaracin de plantilla
int dimension;
public:
T* mVptr;
mVector(int n = 1) {
// constructor por defecto
dimension = n;
mVptr = new T[dimension];
}
~mVector() { delete [] mVptr; }
// destructor
T& operator[](int i) { return mVptr[i]; }
void showmem (int);
};
template<class T> void mVector<T>::showmem (int i) {
// L16:
if((i >= 0) && (i <= dimension)) mVptr[i].showV();
else cout << "Argumento incorrecto! pruebe otra vez" << endl;
}
Observe que aparte del cambio de la declaracin en L1, se han sustituido las ocurrencias
de Vector (un tipo concreto) por el parmetro T. Observe tambin la definicin de showmem() en
L16, que se realiza off-line con la sintaxis de una funcin genrica (
4.12.1).
2.1 Recordemos que en estas expresiones, el especificador class puede ser sustituido
por typename (
3.2.1e), de forma que la expresin L1 puede ser sustituida por:
tamplate<typename T> class mVector {
...
};
// L1-bis
Tambin que la definicin puede corresponder a una estructura (recordemos que son un tipo de
clase en las que todos sus miembros son pblicos). Por ejemplo:
template<class Arg> struct all_true : public unary_function<Arg, bool> {
bool operator()(const Arg& x){ return 1; }
};
2.2 Ejemplo-2
En la pgina adjunta se incluye un ejemplo operativo. Tomando como punto de partida la versin
definitiva de mVector (
4.9.18d1), se ha reproducido el mismo programa, pero cambiando el
diseo, de forma que mVector es ahora una clase genrica en vez de una clase especfica (
Ejemplo-2).
no est permitido
4.12.1). Otra es que no est permitido declarar los destructores como
funciones genricas. Adems, los especificadores <T> referidos a mVector dentro de la propia
definicin son redundantes.
Estas consideraciones hacen que los prototipos puedan ser dejados como sigue (los datos
faltantes pueden deducirse del contexto):
template<class T> class mVector {
// Clase genrica
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 showmem (int);
// funcin auxiliar
void show ();
// funcin auxiliar
};
2.3.1 Las definiciones de mtodos realizadas off-line (fuera del cuerpo de una plantilla) deben ser
declaradas explcitamente como funciones genricas (
4.12.1). Por ejemplo:
template <class T> void mVector<T>::showmem (int i) {
...
}
2.3.2 Ejemplo-3
Observe la sintaxis del siguiente ejemplo. Es igual que el anterior (Ejemplo-2
de que en este, las funciones-miembro se definen off-line (
Ejemplo-3).
), con la diferencia
Hemos sealado que, por su propia naturaleza, los mtodos de clases genricas son a su vez
(implcitamente) funciones genricas con los mismos parmetros que la clase, pero pueden ser
adems funciones genricas explcitas (que dependan de parmetros distintos de la plantilla a que
pertenecen):
template<class X> class A {
template<class T> void func(T& t);
genrica
...
// clase genrica
// mtodo genrico de clase
}
Segn es usual, la definicin del miembro genrico puede efectuarse de dos formas: on-line (en el
interior de la clase genrica
Ejemplo-1), y off-line (en el exterior
Ejemplo-6).
3 Observaciones:
La definicin de una clase genrica puede suponer un avance importante en la definicin de
clases relacionadas, sin embargo son pertinentes algunas advertencias:
3.1 Las clases genricas son entes de nivel superior a las clases concretas. Representan para
las clases normales (en este contexto preferimos llamarlas clases explcitas) lo mismo que las
funciones genricas a las funciones concretas. Como aquellas, solo tienen existencia en el cdigo.
Como el mecanismo de plantillas C++ se resuelve en tiempo de compilacin, ni en el fichero
ejecutable ni durante la ejecucin existe nada parecido a una clase genrica, solo existen
especializaciones (ver a continuacin 4
). En realidad la clase genrica que se ve en el cdigo,
acta como una especie de "macro" que una vez ejecutado su trabajo en la fase de compilacin,
desaparece de escena.
Como ha sealado algn autor, el mecanismo de plantillas representa una especie de polimorfismo
en tiempo de compilacin, similar al que proporciona la herencia de mtodos virtuales en tiempo de
ejecucin.
3.2 Aconsejamos realizar el diseo y una primera depuracin con una clase explcita antes de
convertirla en una clase genrica. Es ms fcil imaginarse el funcionamiento referido a un tipo
concreto que a entes abstractos. Adems es ms fcil entender los problemas que pueden
presentarse si se maneja una imagen mental concreta [1]. En estos casos es ms sencillo ir de lo
particular a lo general.
3.3 Contra lo que ocurre con las funciones genricas, en la instanciacin de clases genricas el
compilador no realiza ninguna suposicin sobre la naturaleza de los argumentos a utilizar, de
modo que se exige que sean declarados siempre de forma explcita. Por ejemplo:
mVector<char> mv1;
mVector mv2 = mv1;
mVector<char> mv2 = mv1;
// Error !!
// Ok.
a los valores por defecto de los argumentos). La consecuencia es que en estos casos el
compilador tampoco realiza ninguna suposicin sobre los argumentos a utilizar.
3.4 Las clases genricas pueden ser utilizadas en los mecanismos de herencia. Por ejemplo:
template <class T> class Base { ... };
template <class T> class Deriv : public Base<T> {...};
Por ejemplo, es posible conseguir que una clase derive de una base o de otra segn las
circunstancias:
#include <iostream>
using namespace std;
class BaseA {
public:
BaseA() { cout << "BaseA"; }
};
class BaseB {
public:
BaseB() { cout << "BaseB"; }
};
template <typename T> class Derived : public T {
public:
Derived() { cout << "::Derived" << endl; }
};
int main() {
Derived<BaseA> ob1;
Derived<BaseB> ob2;
return EXIT_SUCCESS;
}
Como se habr figurado, la salida es:
BaseA::Derived
BaseB::Derived
El punto a destacar es que en el momento de instanciar el objeto es cuando se decide cual ser la
clase antecesora, lo que se consigue mediante la definicin de la clase Derived.
3.5 Con ciertas limitaciones, las clases genricas pueden simular el comportamiento de las
funciones virtuales (
4.11.8a). Consideremos el siguiente ejemplo [6]:
#include <iostream>
using namespace std;
class Base {
public:
virtual void foo() { cout << "foo in Base" << endl; }
El comportamiento de esta jerarqua puede ser mimetizado utilizando una clase genrica:
#include <iostream>
using namespace std;
template <typename T> class Base {
public:
void foo() { cout << "foo in Base" << endl; }
void callFoo() {
T* tPtr = static_cast<T*>(this);
tPtr->foo();
}
};
class Derived : public Base<Derived> {
public:
void foo() { cout << "foo in Derived" << endl; }
};
int main() {
Derived obj;
obj.callFoo();
return 0;
}
Como puede verse, el resultado es el mismo en ambos diseos. La ventaja del segundo frente al
primero es que el cdigo es ms compacto y rpido que el primero, debido a que no tiene
necesidad de utilizar el mecanismo de enlazado dinmico de las funciones virtuales (
1.4.4).
El programa tiene dos puntos interesantes: el primero en la definicin de la clase derivada, donde
podemos comprobar que puede derivar de una clase genrica, y que en este caso, le indicamos al
compilador que utilizaremos una del mismo tipo que la clase derivada. El segundo es la definicin
de la funcin callFoo. El puntero this (puntero a la clase Base) es convertido en un puntero a la
clase derivada, ya que el tipoDerived es pasado a la clase base a travs del parmetro T de la
plantilla. De esta forma, es invocada la funcin foo de la clase derivada, ya que el puntero seala
a dicho objeto.
En este caso, durante la compilacin, la primera sentencia crea una instancia (funcin-clase
4.11.5) de mVector especfica para miembrosVector; que a su vez crear un objeto-clase en
tiempo de ejecucin que ser el encargado de instanciar los objetos mV1 y mV2. En este
contextomVector<Vector> representa la clase que genera los objetos mV1 y mV2. La segunda
sentencia es anloga, aunque para complejos.
Observe que si mVector es una clase genrica, el identificador mVector debe ir acompaado
siempre por un tipo T entre ngulos ( <T> ), ya que los ngulos vacos ( <> ) no pueden aparecer
solos, excepto en algunos casos de la definicin de la plantilla o cuando tenga valores por defecto.
Nota: como veremos a continuacin
, las clases genricas pueden tener argumentos por
defecto, en cuyo caso, el tipo T puede omitirse, pero no los ngulos <>. Por ejemplo:
template<class T = int> class mVector {/* ... */}; // valor int por
defecto
...
mVector<char> mv1;
// Ok. argumento char explcito
mVector<> mv2;
// Ok. argumento int implcito (valor por defecto)
Cada instancia de una clase genrica es realmente una clase, y sigue las reglas generales de las
clases. Como se ver a continuacin
, pueden recibir referencias y punteros, y dispone de su
6 Argumentos de la plantilla
La declaracin de clases genricas puede incluir una lista con varios parmetros. Estos pueden ser
casi de cualquier tipo: complejos; fundamentales (
2.2), por ejemplo un int, o incluso otra clase
genrica (plantilla). Adems, en todos los casos pueden presentar valores por defecto. Ejemplo:
template<class T, int dimension = 128> class mVector { ... };
En ocasiones, cuando el argumento de una clase genrica es a su vez otra clase genrica debe
tenerse en cuenta cierta singularidad sintctica. Considere el siguiente ejemplo:
template <class T> class Point {
public:
T x, y, z;
};
template <class T> class Vector {
public:
T p1, p2;
};
int main() {
// ================
Vector<Point<int> > v1;
// oberseve el espacio > >
return 0;
}
En estos casos, algunos compiladores necesitan que se incluya el espacio entre los smbolos > en
la declaracin del argumento. De no hacerlo as, el compilador podra interpretarlo como el
operador de manejo de bits (left shift
4.9.3) con el consiguiente mensaje de error. En el caso
del compilador GNU cpp, el mensaje obtenido es muy ilustrativo: 12 D:\LearnC\main.cpp
`>>' should be `> >' within a nested template argument list.
6.1 En la instanciacin de clases genricas, los valores de los parmetros que no sean tipos
complejos deben ser constantes o expresiones constantes (
3.2.3). Por ejemplo:
const int K = 128;
int i = 256;
mVector<int, 2*K> mV1;
mVector<Vector, i> mV2;
// OK
// Error: i no es constante
Este tipo de parmetros constantes son adecuados para establecer tamaos y lmites. Por
ejemplo:
template <class T, int max = 128 > class Matriz {
...
int dimension;
T* ptr;
Matriz (int d = 0) {
// constructor
dimension = d > max ? max : d;
ptr = new T [dimension];
}
...
};
Sin embargo, por su propia naturaleza de constantes, cualquier intento posterior de alterar su valor
genera un error.
Cuando es necesario pasar valores numricos, enteros o fraccionarios, para los que pueda existir
ambigedad en el tipo, se aconseja incluir sufijos (
3.2.3b y
3.2.3c). Por ejemplo, en el caso
de instanciar un objeto para la plantilla anterior:
Matriz<Vector, 256U> mV1;
6.2 Los argumentos pueden ser otras plantillas, pero solo clases genricas.; Las funciones
genricas no estn permitidas:
template <class T, template<class X> class C> class MatrizC { // Ok.
...
};
template <class T, template<class X> void Func(X a)> class MatrizF { //
Error!!
...
};
6.3 Tenga en cuenta que no existe algo parecido a un mecanismo de "sobrecarga" de las clases
genricas paralelo al de las funciones genricas. Por ejemplo, las siguientes declaraciones
producen un error de compilacin.
template<class T> class mVector { ... };
template<class T, int dimension> class mVector { ... };
Error: Number of template parameters does not match in redeclaration of
'Matriz<T>'.
// Ok.
...
Matriz<int,5>* ptrMi5 = &m2
Matriz<char,5>* ptrMch5 = &m2;
Matriz<char,1>* ptrMch1 = &m2;
Matriz<char,1>* ptrMch1 = &m3;
ptrMch5->show();
puntero
Matriz<char> m4 = *ptrMch1;
//
//
//
//
//
//
//
//
//
// Ok. referencia
// Ok.
// Ok. invocacin de mtodo mediante
Comentario
Observe que la asignacin L10 exige que la clase genrica Matriz tenga definido el constructorcopia y que est sobrecargado el operador de indireccin * para los miembros de la clase (
4.9.11).
Merecen especial atencin las sentencias L11/L14. En este caso se trata de punteros-a-miembros
de clases implcitas. El tipo de clase est definido en los parmetros. Observe que fptr1 es
puntero-a-mtodo-de-clase-Matriz<char, 5> [2], y no puede referenciar un mtodo de m3, que es
un objeto de tipo Matriz<char,1>.
Nota: para comprender cabalmente las sentencias anteriores, puede ser de gran ayuda un
repaso previo a los captulos: Punteros a Clases (
4.2.1f) y Punteros a Miembros (
4.2.1g).
8.2 Ejemplo-5
En este ejemplo se muestra la utilizacin de punteros a clases implcitas, as como de la
sobrecarga del operador de indireccin (
Ejemplo-5).
Clases genricas
Ejemplo
En realidad se trata de la continuacin de un ejemplo anterior (
Ejemplo-4). All se present un
problema con una asignacin entre objetos de tipo distinto. Aqu se muestra una posible solucin
mediante un "Casting" (
4.9.9d) que permita vencer (por la "fuerza bruta") la resistencia del
mecanismo de comprobacin de tipos del compilador.
El ejemplo muestra tambin una serie de aspectos interesantes sobre el uso de punteros a clases
genricas y a sus miembros. Algunos de los operadores definidos para la clase no son realmente
necesarios, se han mantenido para respetar al mximo el diseo original. Sin embargo, ha sido
necesario incluir una definicin sobrecargada del operador de indireccin * para poder utilizarlo
con miembros de la clase.
#include <iostream>
using namespace std;
template <class T, int dim = 1> class Matriz {
int dimension;
public:
T* mptr;
Matriz operator* () { return *this; }
// Operador de indireccin
Matriz& operator=(const Matriz& m) {
// Operador de asignacin
delete [] mptr;
dimension = m.dimension;
mptr = new T [dimension];
for(int i = 0; i<dimension; i++) mptr[i]= m.mptr[i];
return *this;
}
Matriz operator+ (const Matriz& m) {
// operador suma binaria
Matriz mt(m.dimension + this->dimension);
for(int i = 0; i<this->dimension; i++) mt.mptr[i]= this->mptr[i];
for(int j = 0; j<this->dimension; j++) mt.mptr[j+i]= m.mptr[j];
return mt;
};
Matriz() {
// constructor por defecto
dimension = dim > 64000 ? 64000 : dim;
mptr = new T [dimension];
}
~Matriz() { delete [] mptr; }
// destructor
Matriz(const Matriz& m) {
// constructor-copia
dimension = m.dimension;
mptr = new T [dimension];
for(int i = 0; i<dimension; i++) mptr[i]= m.mptr[i];
}
T& operator[](int i) { return mptr[i]; }
void show () {
// Funcin auxiliar
cout << "Matriz de: " << dimension << " elementos." << endl;
for (int i = 0; i<dimension; i++) {
cout << i << "- " << mptr[i] << "; ";
}
cout << "\n";
}
};
void main() {
// =====================
Matriz<char, 6> m2;
// M.1:
m2[0] ='A'; m2[1] ='E'; m2[2] ='I'; m2[3] ='O'; m2[4] ='U'; m2[5]
='\0';
Matriz<char, 6>* ptrMc6 = &m2;
// M.3: Ok. puntero a clase
ptrMc6->show();
// M:4: Ok. invocacin de mtodo
(salida 1)
//Matriz<char> m3 = m2;
M.5: antiguo Error
//
//
//
//
//
}
Salida:
Matriz de: 6 elementos.
S1
0- A; 1- E; 2- I; 3- O; 4- U; 5- ;
Matriz de: 6 elementos.
S2
0- A; 1- E; 2- I; 3- O; 4- U; 5- ;
AEIOU
S3
A
S4
Matriz de: 6 elementos.
S5
0- A; 1- E; 2- I; 3- O; 4- U; 5- ;
Comentario
En M.1 se instancia una matriz de caracteres de 6 elementos, que son inicializados a valores
concretos en M.2. Por conveniencia para la salida S3, se ha alargado el tamao original de esta
matriz, aadindole un elemento con un carcter nulo.
En M.3 se declara e inicia el puntero ptrMc6; se trata de un puntero a la clase implcita
Matriz<char, 6> que seala al objeto m2 (cuyo tipo coincide con las exigencias del puntero). En la
lnea siguiente se utiliza este puntero para invocar el mtodo show del objeto (salida S1).
La sentencia M.5 es anloga a la que causaba el error en el ejemplo original. El error es debido a
que se pretende la invocacin del constructor-copia con un argumento de tipo distinto al esperado.
M.6 muestra la solucin dada en aquel caso. En realidad se trata de una solucin a medias, ya
que m3 se crea con tipo distinto al pretendido inicialmente (hemos mantenido aqu ambas
sentencias como recordatorio y comparacin con la nueva solucin).
Las sentencias M8 es la nueva solucin. Observe que el puntero ptrMc6 creado en M.3, seala a
la matriz m2 (que se pretende acte como Rvalue en la asignacin fallida de M.5). Ahora se vuelve
a intentar la asignacin, pero sealando el objeto m2 mediante la indireccin de su puntero.
Observe que una asignacin "tal cual" del tipo:
Matriz<char> m4 = *ptrMc6;
no hubiese sido posible; aunque bajo otra forma sintctica, sigue siendo la misma asignacin de
M.5. Para salvar el obstculo obligamos al compilador a realizar una conversin forzada del tipo
sealado por ptrMc6 (reinterpret_cast
4.9.9d). En M.11 se muestra finalmente el resultado
obtenido (salida S2).
En M.9 se crea otro puntero anlogo al de M.3, y se pretende que seale al objeto m4,
producindose un error. La razn vuelve a estar en la cuestin de los tipos. El nuevo objeto es
puntero a la clase implcita Matriz<char, 6>, mientras que el objeto m4 que se pretende sealar es
de tipo Matriz<char, 1>.
En M.10 se crea e inicia sin novedad un puntero de tipo adecuado al objeto que se pretende
sealar, y en M.11 se utiliza para invocar el mtodoshow sobre el objeto m4, lo que produce la
salida S2. Es muy significativo que para el compilador, m4 es tipo Matriz<char, 1>, a pesar de que
"realmente" responde a una matriz de 6 elementos. Este es justamente uno de los peligros de un
modelado "por la tremenda" como el utilizado. Probablemente, si se tratara de un programa
grande, antes que despus tendramos problemas (C++ no necesita que el programador se busque
problemas engaando al compilador, se basta l solito para encontrarlos :-)
En M.13 se define un puntero a miembro mpt1 (
4.2.1g). Concretamente al tipo puntero-achar (char*). En la siguiente sentencia este puntero es asignado al nico miembro de la clase
implcita que responde a la especificacin, el miembro mptr. A pesar de la asignacin, en este
momentompt1 es en realidad un puntero genrico (es una curiosa caracterstica de los punteros a
miembros no estticos). Seala al miembro de cualquierobjeto que responda a su especificacin.
Nota: es importante sealar que este puntero solo puede sealar a miembros de su propio tipo. Si,
por ejemplo, se instancia un objeto:
Matriz<int, 6> m1;
La asignacin:
mpt1 = &Matriz<int, 6>::mptr;
producira un error, ya que el puntero mpt1 y el miembro sealado son de tipos distintos. Para
sealar al miembro mptr del objeto m1, es preciso que el puntero responda a la siguiente
declaracin:
int* Matriz<int,6>::* mpt1;
Al final se aaden algunas consideraciones sobre la cuestin aqu apuntada.
La sentencia M.15 aplica la indireccin del puntero "genrico" mpt1 sobre el objeto m2 [1] (observe
la notacin utilizada). En realidad pedimos al compilador que muestre el objeto sealado por el
puntero. Recordemos que el objeto es un puntero-a-char, y en estas circunstancias, se muestra la
cadena hasta el primer carcter nulo (salida S3); esta caracterstica es herencia del C clsico,
donde las cadenas alfanumricas terminan en un carcter nulo.
En M.16 se ha utilizado la notacin adecuada para obtener un solo elemento (salida S4).
Utilizando un subndice adecuado puede accederse cualquier otro.
La sentencia M.18 es parecida a M.13, aunque en este caso declara un puntero a funcin-miembro
(fpt1). En la siguiente, el puntero es asignado al mtodo show de la clase implcita
correspondiente. Finalmente, el mtodo (accedido mediante la indireccin de su puntero), es
aplicado sobre el objeto m2, dando lugar a la salida S5.
En el ejemplo se han utilizado dos de las tres posibilidades de invocacin de un mtodo sobre un
objeto:
ptrMc6->show();
indirecto)
(m2.*fpt1)();
m2.show();
Corolario:
La cuestin suscitada con motivo del puntero-a-miembro mpt1 plantea en realidad una interesante
cuestin sobre los punteros a miembros de clases genricas. El asunto puede ser esquematizado
como sigue:
template <class T> class Cls {
// clase genrica
T* mem;
// puntero-a-tipo de la clase ( puntero-a-Cls<T> )
Cls<T>*
...
}
...
Cls<T1> obj1;
// instancia de clase implcita Cls<T1>
Cls<T2> obj2;
// instancia de clase implcita Cls<T2>
... etc.
La cuestin es que si queremos un puntero al miembro mem, debemos declararlo con la sintaxis
adecuada:
T1* Cls<T1>::* ptr1 = &Cls<T1>::mem;
T2* Cls<T2>::* ptr2 = &Cls<T2>::mem;
...
// etc.
Sin embrago, el lenguaje no permite este tipo de definiciones siendo T una variable (las plantillas
solo pueden definirse para funciones y clases).