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

En realidad se trata de la continuación de un ejemplo anterior ( Ejemplo-4). Allí se presentó un problema con una asignación entre objetos de tipo distinto. Aquí se muestra una posible solución mediante un "Casting" ( 4.9.9d) que permita vencer (por la "fuerza bruta") la resistencia del mecanismo de comprobación de tipos del compilador.

El ejemplo muestra también una serie de aspectos interesantes sobre el uso de punteros a clases genéricas y a sus miembros. Algunos de los operadores definidos para la clase no son realmente necesarios, se han mantenido para respetar al máximo el diseño original. Sin embargo, ha sido necesario incluir una definición sobrecargada del operador de indirección * 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 indirección
  Matriz& operator=(const Matriz& m) {    // Operador de asignación
    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 () {                 // 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<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. invocación de método (salida 1)
//Matriz<char> m3 = m2;                M.5: antiguo Error
//Matriz<char, 6> m3 = *ptrMc6;        M.6: antigua solución

  Matriz<char> m4 = *(reinterpret_cast<Matriz<char,1>*> (ptrMc6));   // M.8:
  Matriz<char, 6>* ptrMc6b = &m4;   // M.9: Error!!
  Matriz<char>* ptrMc1a = &m4;      // M.10: Ok.
  ptrMc1a->show();                  // M.11: Ok. (salida 2)

  char* Matriz<char,6>::* mpt1;     // M.13 puntero a miembro
  mpt1 = &Matriz<char, 6>::mptr;    // M.14: Ok. asignación a un miembro
  cout << m2.*mpt1 << endl;         // M.15: Salida 3
  cout << (m2.*mpt1)[0] << endl;    // M.16: Salida 4

  void (Matriz<char,6>::* fpt1)();  // M.18: puntero a función miembro
  fpt1 = &Matriz<char,6>::show;
  (m2.*fpt1)();                     // M.20: Salida 5
}

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 tamaño original de esta matriz, añadiéndole un elemento con un carácter nulo.

En M.3 se declara e inicia el puntero ptrMc6; se trata de un puntero a la clase implícita Matriz<char, 6> que señala al objeto m2 (cuyo tipo coincide con las exigencias del puntero). En la línea siguiente se utiliza este puntero para invocar el método show del objeto (salida S1).

La sentencia M.5 es análoga a la que causaba el error en el ejemplo original. El error es debido a que se pretende la invocación del constructor-copia con un argumento de tipo distinto al esperado. M.6 muestra la solución dada en aquel caso. En realidad se trata de una solución a medias, ya que m3 se crea con tipo distinto al pretendido inicialmente (hemos mantenido aquí ambas sentencias como recordatorio y comparación con la nueva solución).

Las sentencias M8 es la nueva solución. Observe que el puntero ptrMc6 creado en M.3, señala a la matriz m2 (que se pretende actúe como Rvalue en la asignación fallida de M.5). Ahora se vuelve a intentar la asignación, pero señalando el objeto m2 mediante la indirección de su puntero.

Observe que una asignación "tal cual" del tipo:

Matriz<char> m4 = *ptrMc6;

no hubiese sido posible; aunque bajo otra forma sintáctica, sigue siendo la misma asignación de M.5. Para salvar el obstáculo obligamos al compilador a realizar una conversión forzada del tipo señalado 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 análogo al de M.3, y se pretende que señale al objeto m4, produciéndose un error. La razón vuelve a estar en la cuestión de los tipos. El nuevo objeto es puntero a la clase implícita Matriz<char, 6>, mientras que el objeto m4 que se pretende señalar 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 señalar, y en M.11 se utiliza para invocar el método show 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 después tendríamos problemas (C++ no necesita que el programador se busque problemas engañando 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-a-char (char*). En la siguiente sentencia este puntero es asignado al único miembro de la clase implícita que responde a la especificación, el miembro mptr. A pesar de la asignación, en este momento mpt1 es en realidad un puntero genérico (es una curiosa característica de los punteros a miembros no estáticos). Señala al miembro de cualquier objeto que responda a su especificación.

Nota: es importante señalar que este puntero solo puede señalar a miembros de su propio tipo. Si, por ejemplo, se instancia un objeto:

Matriz<int, 6> m1;

La asignación:

mpt1 = &Matriz<int, 6>::mptr;

produciría un error, ya que el puntero mpt1 y el miembro señalado son de tipos distintos. Para señalar al miembro mptr del objeto m1, es preciso que el puntero responda a la siguiente declaración:

int* Matriz<int,6>::* mpt1;

Al final se añaden algunas consideraciones sobre la cuestión aquí apuntada.


La sentencia M.15 aplica la indirección del puntero "genérico" mpt1 sobre el objeto m2 [1] (observe la notación utilizada). En realidad pedimos al compilador que muestre el objeto señalado por el puntero. Recordemos que el objeto es un puntero-a-char, y en estas circunstancias, se muestra la cadena hasta el primer carácter nulo (salida S3); esta característica es herencia del C clásico, donde las cadenas alfanuméricas terminan en un carácter nulo.

En M.16 se ha utilizado la notación adecuada para obtener un solo elemento (salida S4).  Utilizando un subíndice adecuado puede accederse cualquier otro.

La sentencia M.18 es parecida a M.13, aunque en este caso declara un puntero a función-miembro (fpt1). En la siguiente, el puntero es asignado al método show de la clase implícita correspondiente. Finalmente, el método (accedido mediante la indirección 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 invocación de un método sobre un objeto:

ptrMc6->show();   // invocación mediante puntero-a-clase (selector indirecto)
(m2.*fpt1)();     // invocación mediante puntero-a-miembro (indirección)
m2.show();        // invocación mediante selector directo de miembro

Corolario:

La cuestión suscitada con motivo del puntero-a-miembro mpt1 plantea en realidad una interesante cuestión sobre los punteros a miembros de clases genéricas. El asunto puede ser esquematizado como sigue:

template <class T> class Cls {    // clase genérica
  T* mem;         // puntero-a-tipo de la clase ( puntero-a-Cls<T> ) Cls<T>*
  ...
}
...
Cls<T1> obj1;      // instancia de clase implícita Cls<T1>
Cls<T2> obj2;      // instancia de clase implícita Cls<T2>
... etc.

La cuestión es que si queremos un puntero al miembro mem, debemos declararlo con la sintaxis adecuada:

T1* Cls<T1>::* ptr1 = &Cls<T1>::mem;      // para clase Cls<T1>
T2* Cls<T2>::* ptr2 = &Cls<T2>::mem;      // para clase Cls<T2>
...            // etc.

Evidentemente se necesitan tantas declaraciones como especializaciones distintas puedan derivarse de la plantilla. En realidad se necesitaría una definición parametrizada del puntero a miembro. Algo parecido a:

T* Cls<T>::* ptrn = &Cls<T>::mem;         // para clase genérica Cls<T>

Sin embrago, el lenguaje no permite este tipo de definiciones siendo T una variable (las plantillas solo pueden definirse para funciones y clases).

  Inicio.


[1]  Utilizamos esta terminología para mantener un paralelismo con la usual en estos casos, ya que la sentencia  m2.show() se define coloquialmente como "aplicar el método show sobre el objeto m2"; también: "invocar el método del objeto". Parece razonable suponer que la sentencia m2.*mpt1 podría enunciarse como "aplicar la indirección del puntero mpt1 sobre el objeto m2", o "aplicar la indirección del puntero del objeto".

Preferimos la primera para resaltar que, en realidad, el puntero es bastante independiente del objeto. Incluso se podría aplicar a un objeto mx distinto:

cout << mx.*mpt1 << endl.