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]


Clases genéricas

Ejemplo

Este ejemplo muestra las diferencias obtenidas con tres versiones casi idénticas del mismo ejecutable. Se ilustra como estas diferencias son debidas a la "cuestión de los tipos".

El programa utiliza una clase Matriz cuyos objetos son matrices de diverso tipo. Esta clase ha sido dotada de los recursos para poder establecer unas operaciones mínimas con sus objetos: constructor por defecto; constructor-copia; destructor; operador de asignación; operador de subíndice; operador suma, y finalmente una función auxiliar para mostrar los miembros de la matriz.

Nota: la suma se presenta en realidad como una concatenación de matrices. El resultado equivale a poner la segunda a continuación de la primera. Aunque no hemos definido el operador de identidad para la clase (==), aparentemente esta suna no es conmutativa (al menos desde un punto de vista geométrico). Observe que el segundo bucle (for) de la función operator+  supone que el índice i del primer bucle es todavía visible ( 4.10.3).

Se presentan tres versiones: en la primera, se utiliza una clase explícita; en la segunda esta clase se transforma en una clase genérica, y en la tercera se incluye un segundo argumento para evitar tener que incluirlo en el constructor.

§1  Una clase explícita

En esta primera versión, la clase Matriz permite instanciar matrices de enteros (#define de L.3) cuyo tamaño por defecto es 1 (argumento por defecto del constructor).


#include <iostream>
using namespace std;
#define T int              // L.3

class Matriz {             // Una clase explícita
  int dimension;
  public:
  T* mptr;
  Matriz& operator=(const Matriz& m) {   // Operador =
    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) {   // operator +
    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[i+j]= m.mptr[j];
    return mt;
  };
  Matriz(int dim =1) {                    // constructor por defecto
    dimension = 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 () {                          // función auxiliar
    cout << "Matriz de: " << dimension << " elementos." << endl;
    for (int i = 0; i<dimension; i++) {
      cout << i << "- " << mptr[i] << "; ";
    }
    cout << "\n";
  }
};

void main() {          // =====================
  Matriz m1 = Matriz(5);           // M.1
  m1[0]= 0; m1[1] = 1; m1[2] = 2; m1[3] = 3; m1[4] = 4;
  m1.show();
  Matriz m2 = Matriz(5);           // M.4
  m2[0] ='A'; m2[1] ='E'; m2[2] ='I'; m2[3] ='O'; m2[4] ='U';   // M.5
  m2.show();
  Matriz m3 = m2;     m3.show();   // M.7
  Matriz m4;          m4.show();   // M.8
  m4 = m1 + m2;       m4.show();   // M.9
}

Salida:

Matriz de: 5 elementos.                §1 S1
0- 0; 1- 1; 2- 2; 3- 3; 4- 4;
Matriz de: 5 elementos.                §1 S2
0- 65; 1- 69; 2- 73; 3- 79; 4- 85;
Matriz de: 5 elementos.                §1 S3
0- 65; 1- 69; 2- 73; 3- 79; 4- 85;
Matriz de: 1 elementos.                §1 S4
0- 4302136;
Matriz de: 10 elementos.               §1 S5
0- 0; 1- 1; 2- 2; 3- 3; 4- 4; 5- 65; 6- 69; 7- 73; 8- 79; 9- 85;

Comentario

Las sentencias M.1 y M.4 crean sendas matrices de cinco elementos m1 y m2, mediante invocaciones explícitas al constructor. Posteriormente se inicializan (con valores numéricos la primera y caracteres la segunda), y se muestran los resultados (salidas S1 y S2). Puesto que los miembros de m1 y m2 están definidos como "enteros", el compilador realiza una conversión en las asignaciones de M.5, de modo que los caracteres son transformados a sus correspondientes valores ASCII ( 2.2.1a).

En las sentencias M.7 se instancia un nuevo elemento m3, con los valores de uno anterior (m2) y se muestra el resultado. Esta operación supone una invocación implícita al constructor-copia. El resultado es el esperado (salida S3).

En M.8 se crea una matriz y se muestra su contenido. Aquí se realiza una invocación implícita al constructor con el argumento por defecto. Como muestra S4, el tamaño es 1 y su valor basura (el único elemento, m4[0] no ha sido inicializado).

En M.9 se utilizan los operadores suma y asignación. El resultado de la suma (concatenación) de m1 + m2 es asignado a m4, y es el que cabría esperar (S5).

§2  Una clase genérica

En esta segunda versión, la clase explícita del ejemplo anterior se ha sustituido por una clase genérica. Observe que los cambios en el espacio global se han reducido a comentar el define de L.3 y a cambiar la declaración de L.5. Se ha cambiado igualmente la sintaxis de las declaraciones de m1 a m4 en la función main para adecuarlas al hecho de que ahora Matriz es una plantilla.


#include <iostream>
using namespace std;
// #define T int           L.3

template <class T> class Matriz {        // L.5 Una clase genérica
  int dimension;
  public:
  T* mptr;
  Matriz& operator=(const Matriz& m) {   // Operador =
    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) {   // operator +
    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[i+j]= m.mptr[j];
    return mt;
  };
  Matriz(int dim =1) {                   // constructor por defecto
    dimension = 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 () {                          // función auxiliar
    cout << "Matriz de: " << dimension << " elementos." << endl;
    for (int i = 0; i<dimension; i++) {
      cout << i << "- " << mptr[i] << "; ";
    }
    cout << "\n";
  }
};

void main() {          // =====================
  Matriz<int> m1 = Matriz<int>(5);     // M.1
  m1[0]= 0; m1[1] = 1; m1[2] = 2; m1[3] = 3; m1[4] = 4;
  m1.show();
  Matriz<char> m2 = Matriz<char>(5);   // M.4
  m2[0] ='A'; m2[1] ='E'; m2[2] ='I'; m2[3] ='O'; m2[4] ='U';  // M.5
  m2.show();
  Matriz<char> m3 = m2;  m3.show();    // M.7
  Matriz<char> m4;       m4.show();    // M.8
  m4 = m1 + m2;          m4.show();    // M.9  Error!!
  m4 = m2;               m4.show()     // M.9bis
}

Salida (despues de eliminada la sentencia M.9):

Matriz de: 5 elementos.             §2 S1
0- 0; 1- 1; 2- 2; 3- 3; 4- 4;
Matriz de: 5 elementos.             §2 S2
0- A; 1- E; 2- I; 3- O; 4- U;
Matriz de: 5 elementos.             §2 S3
0- A; 1- E; 2- I; 3- O; 4- U;
Matriz de: 1 elementos.             §2 S4
0- T;
Matriz de: 5 elementos.             §2 S5
0- A; 1- E; 2- I; 3- O; 4- U;

Comentario

Las declaraciones M.1, M.4, M.7 y M.8 incluyen ahora el argumento correspondiente.  En M.1 se ha creado una matriz de enteros utilizando el argumento <int> tanto en la declaración del objeto (Lvalue), como en la invocación explícita al constructor (Rvalue). En todos los demás casos se instancian matrices de caracteres.

Ahora los miembros de m2 son tipo char, por lo que las asignaciones de M.5 no necesitan de ninguna transformación de parte del compilador, y como puede comprobarse en S2, los valores son almacenados como auténticos tipos char.  En el caso de m4, el valor basura es mostrado también como char (S4).

La única sorpresa la proporciona la sentencia M.9, donde el compilador avisa que el operador suma no está implementado para sumar matrices numéricas ( m1 ) con matrices de caracteres ( m2 ): Error ... 'operator+' not implemented in type 'Matriz<int>' for arguments of type 'Matriz<char>'. En cambio, la asignación no presenta problemas (entre matrices del mismo tipo), como se comprueba con la sentencia alternativa utilizada (M.9bis).


Es de la mayor importancia entender que el compilador ha construido dos clases: una para matrices de enteros, y otra para matrices de caracteres, las designaremos Matriz-int y Matriz-char. Cada una se encarga de instanciar objetos de su tipo específico e incluye sus propios métodos en versión adecuada [1]. El método operator+ de Matriz-int devuelve una matriz de enteros y espera así mismo recibir una matriz de enteros como argumento. El de Matriz.char espera que tanto el valor devuelto como el argumento sean tipo Matriz<char>. Las definiciones de ambas funciones son:

Matriz<int>  operator+(const Matriz<int>&);   // §2a  en la clase Matriz-int
Matriz<char> operator+(const Matriz<char>&);  // §2b  en la clase Matriz-char


En M.9 se invoca la forma §2a. Recordemos que a + b equivale a: a.operator+(b). Es decir, la función operator+ correspondiente al primer miembro. El resumen es que se invoca una función que espera un tipo Matriz<int> y se recibe un tipo Matriz<char>, lo que produce un error en el mecanismo de comprobación de tipos del compilador.

Observe que la función operator+, de la primera versión , que responde a la definición §2a, no produjo ningún error porque, en aquel caso, todas las matrices implicadas eran Matriz<int>.

§3  Plantilla con dos argumentos

En esta tercera versión, la plantilla recibe un segundo argumento que determinará el tamaño del objeto (matriz) generado y que tiene un valor por defecto. Para ello introducimos un pequeño cambio en la definición del constructor, que por su parte, ya no necesita ningún parámetro; además le incluimos un dispositivo de control para evitar que puedan generarse matrices mayores que un valor determinado.

El resto de modificaciones se refieren a la sintaxis de las declaraciones de m1 a m4 en main, porque ahora Matriz recibe dos argumentos.


#include <iostream>
using namespace std;
// #define T int            L.3

template <class T, int dim =1> class Matriz {   // Plantilla
  int dimension;
  public:
  T* mptr;
  Matriz& operator=(const Matriz& m) {   // Operador =
    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) {  // operator +
    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[i+j]= 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 () {                         // función auxiliar
    cout << "Matriz de: " << dimension << " elementos." << endl;
    for (int i = 0; i<dimension; i++) {
      cout << i << "- " << mptr[i] << "; ";
    }
    cout << "\n";
  }
};

void main() {            // =====================
  Matriz<int, 5> m1;                   // M.1
  m1[0]= 0; m1[1] = 1; m1[2] = 2; m1[3] = 3; m1[4] = 4;
  m1.show();
  Matriz<char, 5> m2;                  // M.4
  m2[0] ='A'; m2[1] ='E'; m2[2] ='I'; m2[3] ='O'; m2[4] ='U';   // M.5
  m2.show();
//Matriz<char> m3 = m2;  m3.show();       M.7  Error!!
  Matriz<char, 5> m3 = m2;  m3.show(); // M.7bis
  Matriz<char> m4;       m4.show();    // M.8
//m4 = m1 + m2;          m4.show();       M.9  Error!!
//m4 = m2;               m4.show()        M.9bis
}

Salida (despues de eliminada la sentencia M.9):

Matriz de: 5 elementos.           §3 S1
0- 0; 1- 1; 2- 2; 3- 3; 4- 4;
Matriz de: 5 elementos.           §3 S2
0- A; 1- E; 2- I; 3- O; 4- U;
Matriz de: 5 elementos.           §3 S3
0- A; 1- E; 2- I; 3- O; 4- U;
Matriz de: 1 elementos.           §3 S4
0- t;

Comentario

Las declaraciones M.1y M.4 incluyen ahora el segundo argumento, correspondiente al tamaño de la matriz que se va a instanciar, por lo que ya no es necesaria la invocación explícita al constructor de la clase.

En M.7 obtenemos el primer error: Cannot convert 'Matriz<char,5>' to 'Matriz<char,1>'.  La razón es que hemos instanciado m3 con el valor dim por defecto, y ahora es de tipo Matriz<char, 1>, mientras que m2 es tipo Matriz<char, 5>. El hecho de incluir un segundo argumento implica que ambas matrices sean ahora de tipos distintos.

Recordemos que en la primera versión, m3 y m2 eran tipo Matriz<int>, y en la segunda Matriz<char>. En estos primeros casos el tamaño concreto no influía en el tipo del objeto. El constructor-copia invocado en la sentencia M.7 de la primera y segunda versión tiene las formas:

Matriz(const Matriz<int>& m);       // primera versión
Matriz(const Matriz<char>& m);      // segunda versión

Como en ambos casos hay concordancia entre el argumento formal y el actual, no existen problemas. Sin embargo, el constructor-copia invocado en la sentencia M.7 de esta tercera versión tiene la forma:

Matriz(const Matriz<char, 1>& m);   // tercera versión

y evidentemente no existe concordancia con el argumento actual ( Matriz<char, 5> ) que pretende pasarse. El compilador intenta hacer un "casting" al tipo que sería correcto y el resultado -no posible- es el error indicado. Para resolver el problema cambiamos M.7 por M.7bis, en la que construimos m3 con el tamaño adecuado.

Nota: en el ejemplo adjunto se expone una continuación de este caso, en el que la solución consiste en forzar al compilador a aceptar la diferencia de tipos ( Ejemplo-5).


El segundo error lo obtenemos en M.9bis: Could not find a match for 'Matriz<char,1>::operator =(Matriz<char,5>)'. El motivo es análogo al anterior; puesto que m4 se ha creado también con el valor por defecto (M.8), su tamaño también es 1. La sentencia M.9bis implica una invocación al operador suma que tiene el siguiente aspecto:

Matriz<char,1> operator+ (const Matriz<char, 1>& m);

Pero se necesitaría una versión con la siguiente forma:

Matriz<char,1> operator+ (const Matriz<char, 5>& m);

Ya que el argumento actual que se pretende pasar ( m2 ) es tipo Matriz<char, 5>.  El mensaje del compilador (Builder C++ 5.5) es otra forma de anunciarnos que no puede realizar la conversión adecuada.

  Inicio.


[1]  Recordemos que  las funciones-miembro son a su vez funciones genéricas con los mismos parámetros que la clase genérica a que pertenecen.