El documento describe la sobrecarga del operador de subíndice [] en C++. Explica que este operador se utiliza para acceder a elementos de matrices y que puede sobrecargarse mediante la función "operator[]". A continuación, presenta un ejemplo de clases "Vector" y "mVector" donde mVector contiene una matriz de vectores y sobrecarga el operador [] para permitir el acceso a los elementos vectoriales de la matriz.
El documento describe la sobrecarga del operador de subíndice [] en C++. Explica que este operador se utiliza para acceder a elementos de matrices y que puede sobrecargarse mediante la función "operator[]". A continuación, presenta un ejemplo de clases "Vector" y "mVector" donde mVector contiene una matriz de vectores y sobrecarga el operador [] para permitir el acceso a los elementos vectoriales de la matriz.
El documento describe la sobrecarga del operador de subíndice [] en C++. Explica que este operador se utiliza para acceder a elementos de matrices y que puede sobrecargarse mediante la función "operator[]". A continuación, presenta un ejemplo de clases "Vector" y "mVector" donde mVector contiene una matriz de vectores y sobrecarga el operador [] para permitir el acceso a los elementos vectoriales de la matriz.
Como ejemplo incluimos una versin de la clase anterior en la que sobrecargamos los operadores preincremento y predecremento, pero utilizando la posibilidad 2.1b enunciada al principio . Es decir, mediante una funcin-operador externa que acepte un argumento. Nota: aunque no es necesario, porque la nica propiedad de la clase es pblica, hemos declarado las funciones-operador como friend de la clase. Esto es lo usual, porque as se garantiza el acceso a los miembros, incluso privados, de la clase desde la funcin. #include <iostream> using namespace std;
2.2 Sobrecarga de los post-operadores X++ X-- Los postoperadores incremento ++ y decremento -- solo pueden ser sobrecargados definiendo las funciones-operador de dos formas: 2.2a. Declarando una funcin miembro no esttica que acepte un entero como argumento. Ejemplo: C C::operator++(int); 2.2b. Declarando una funcin no miembro (generalmente friend) que acepte un objeto de la clase y un entero como argumentos (en este orden). Ejemplo: C operator-- (C&, int); Segn lo anterior, y dependiendo de la declaracin, si @ representa un post-operador unitario (++ o --), la expresin x@ puede ser interpretada como cualquiera de las dos formas: 2.2a: x.operator@(int) 2.2b: operator@(x, int) Nota: debemos advertir que la inclusin del entero como argumento es simplemente un recurso de los diseadores del lenguaje para que el compilador pueda distinguir las definiciones de los "pre" y "post" operadores ++ y -- (el argumento no se usa para nada ms). De hecho, las primeras versiones C++ (hasta la versin 2.0 del preprocesador cfront de AT&T), no distinguan entre las versiones sobrecargadas "pre" y "post" de los operadores incremento y decremento. Para ilustrar el proceso, extendemos el ejemplo de la clase Entero sobrecargando los operadores postincremento y postdecremento. Mantenemos la misma lgica que establecimos con los preoperadores: el incremento aumenta al doble el valor de la propiedad x, y el decremento lo disminuye a la mitad. Para la definicin del primero utilizamos la solucin 2.2a, y la 2.2b para el segundo. #include <iostream> using namespace std;
void main () { // =========== Entero e1 = { 6 }, e2; // M.1 e2 = e1++; // M.2 cout << " e1 = " << e1.x << " e2 = " << e2.x << endl; e2 = e1--; // M.4 cout << " e1 = " << e1.x << " e2 = " << e2.x << endl; } Salida: e1 = 12 e2 = 6 e1 = 6 e2 = 12 Comentario En la definicin de los operadores preincremento (L.6) y predecremento (L.7) se ha utilizado la frmula 2.1b : "declarar una funcin no miembro que acepte un argumento". Para postincremento (L.8), se ha utilizado la opcin 2.2a "funcin miembro no esttica que acepte un entero como argumento". Finalmente, para el posdecremento (L.12) se ha utilizado la opcin 2.2b "una funcin no miembro que acepte un objeto de la clase y un entero". Puede comprobarse que las salidas son las esperadas para los operadores. En M.2 se asigna el valor inicial de e1 a e2 (en este momento e1.xtiene el valor 6). A continuacin se incrementa e1, con lo que el valor e1.x pasa a ser 12. Estos son los resultados mostrados en la primera salida. En M.4 el valor inicial de e1 es asignado a e2 (ahora e1.x tiene valor 12). A continuacin e1 es decrementado, con lo que el valor final de e1.xes 6; los resultado se ven en la segunda salida. Es interesante observar que los operadores "post" incremento/decremento presentan una dificultad terica al ser tratados como funciones: se precisa un mecanismo que aplique el resultado exigido, pero devuelva el objeto en su estado "anterior" a la aplicacin del mismo. En realidad las definiciones de las funciones operator++ y operator-- de L.8 y L.21 intentan mimetizar este comportamiento mediante un objeto temporal tmp, que es creado antes que nada con el contenido del objeto sobre el que se aplicar el operador. Observe que en L.9 nos referimos a l mediante *this, mientras que en L.22 es el objeto pasado explcitamente como argumento. Este objeto temporal tmp es el que realmente se devuelve, con lo que la funcin-operador devuelve un objeto anlogo al operando antes de la modificacin, al mismo tiempo que las modifcaciones se realizan sobre el objeto original. En nuestro caso, la modificacin de la propiedad x del operando, se realiza como: x = x + x; // L.10: cuando se trata de una funcin-miembro, y como: e.x = e.x / 2; // L.23: cuando la funcin-operador no es miembro de la clase. Ntese que en este ltimo caso, el objeto pasado como argumento debe serlo como referencia (L.12 y L.21). La razn es que la funcin-operador debe modificar el valor del objeto pasado como argumento ( 4.2.3). Observe tambin que este diseo permite la existencia de varias funciones-operador post- incremento / post-decremento referidas a clases distintas. El mecanismo de sobrecarga permitir al compilador saber de que funcin se trata a travs del anlisis de sus argumentos. Nota: el diseo de los operadores "post" presenta una importante dificultad terica: en ambos casos es necesario devolver un valor, no una referencia. Esto hace que el resultado no pueda ser utilizado al lado izquierdo de una asignacin (como un Lvalue). Es decir, no son posibles asignaciones del tipo: e2++ = e1; // Error: "Lvalue required" por tanto tampoco son posibles expresiones de asignacin compuesta del tipo: e3 = e2++ = e1++; // Error: que s son posibles con los pre-operadores . Esta limitacin es extensiva incluso a los tipos bsicos; tampoco con ellos son posibles expresiones del tipo: int x = 5, y, z; y++ = x; // Error: "Lvalue required" z = y++ = x++; // Error: mientras que las anlogas con preincremento y predecremento s son posibles : z = ++y = ++x; // Ok.
En la pgina adjunta se expone un ejemplo de una tcnica que utiliza la indireccin ( 4.9.16) y la sobrecarga de operadores unarios para simular una sobrecarga de los operadores ++ y -- sobre punteros; algo que como se ha indicado ( 4.9.18), no es en principio posible Ejemplo.
3 Sobrecarga del operador de indireccin Recordemos que el operador de indireccin * ( 4.9.11) es un preoperador unario, por lo que su sobrecarga puede ser efectuada de cualquiera de las formas 2.1a o 2.1b . En la pgina adjunta se incluye un completo ejemplo de su utilizacin ( Ejemplo).
4 Sobrecarga del operador de negacin lgica La sobrecarga del operador ! NOT de negacin lgica puede verse en el epgrafe ( 4.9.18g) junto con las sobrecargas del resto de operadores lgicos (binarios). 4.9.18d Sobrecarga del operador [ ] 1 Sinopsis Recordemos ( 4.9.16) que este operador sirve para sealar subndices de matrices simples y multidimensionales; de ah su nombre, operadorsubndice o de seleccin de miembro de matriz. La expresin: <exp1>[exp2] se define como: *((exp1) + (exp2)) donde exp1 es un puntero y exp2 es un entero o viceversa. Por ejemplo, arrX[3] se define como: *(arrX + 3) o *(3 + arrX), donde arrX es un puntero al primer elemento de la matriz. (arrX + 3) es un puntero al cuarto elemento, y *(arrX + 3) es el valor del cuarto elemento de la matriz. Lo anterior puede sintetizarse en la siguiente relacin: arrX[3] == *(arrX + 3) // 1a
2 Cuando se utiliza con tipos definidos por el usuario, este operador puede ser sobrecargado mediante la funcin operator[ ]( ) ( 4.9.18).Para ilustrarlo con un ejemplo, utilizaremos la clase mVector que contiene una matriz (es una matriz de vectores), y suponemos que los elementos de la matriz son vectores deslizantes de un espacio bidimensional. El diseo bsico es el que se indica: class Vector { // definicin de la clase Vector public: int x, y; };
class mVector { // definicin de la clase mVector public: Vector* mVptr; // L.6: mVector(int n = 1) { // constructor por defecto mVptr = new Vector[n]; // L.8: } ~mVector() { // destructor delete [] mVptr; } }; Comentario La clase Vector tiene solo dos miembros, que suponemos las componentes escalares de cada vector del plano. Por simplicidad hemos supuesto que son int, pero podran ser otros tipos de punto flotante, por ejemplo float o double. Esta clase auxiliar la hemos definido externa e independiente de la clase mVector. Tambin podra utilizarse otro diseo en el que Vector estuviese definida "dentro" de la clase mVector. Las diferencias entre ambos y los criterios de uso se discuten en ( 4.13.2): class mVector { // definicin de la clase mVector ... class Vector { // clase anidada ... }; ... };
La clase mVector tiene un solo miembro; un puntero-a-Vector mVptr (L.6). Tambin definimos un constructor por defecto y un destructor. Observe (L.8) que el constructor del objeto tipo mVector, crea una matriz de objetos tipo Vector del tamao indicado en el argumento (1 por defecto) y la seala con el puntero mVptr. Esta matriz est alojada en memoria persistente ( 4.9.20c) y en cierta forma podramos pensar que es "externa" al objeto, ya que este realmente solo contiene un puntero [1]. Precisamente en razn de esta persistencia, el destructor debe rehusar la memoria asignada a la matriz, pues de otro modo este espacio se perdera al ser destruido el objeto ( 4.9.21). Siguiendo el paradigma de la POO, esta clase deber contener los datos (la matriz) y los algoritmos (mtodos) para manejarla. Deseamos utilizar los objetos de tipo mVector como autnticas matrices, por lo que deberamos poder utilizarlos con lgebra de matrices C++. Utilizando una analoga, si por ejemplo m es una matriz de enteros, sabemos que el lenguaje nos permite utilizar las expresiones siguientes: m[i]; // L.1: acceso a elemento con el operador subndice int x = m[i]; // L.2: asignacin a un miembro de la clase int m[i] = m[j]; // L.3: asignacin a miembro m[i] = 3 * m[j]; // L.4: producto por un escalar m[i] = m[j] * m[k]; // L.5: producto entre miembros En consecuencia, debemos preparar el diseo de la clase mVector de forma que que pueda mimetizarse el comportamiento anterior con sus objetos. Es decir, deben permitirse las siguientes expresiones: mVector m1; m1[i]; // acceso a elemento con el operador subndice Vector v1 = m1[i]; // asignacin a un miembro de la clase Vector m1[i] = m1[j]; // asignacin a miembro m1[i] = 3 * m1[j]; // producto por un escalar m1[i] = m1[j] * m1[k]; // producto entre miembros 3 Operador subndice Para mimetizar este comportamiento con los objetos de la nueva clase empezaremos por poder referenciarlos mediante el operador subndice [ ](L.1 ). Este operador debe recibir un int y devolver el miembro correspondiente de la matriz (recordemos que los miembros son tipo Vector). Como sabemos que debe gozar del doble carcter de Rvalue y Lvalue (L.3 ), deducimos que debe devolver una referencia ( 4.9.18c). A "vote pronto" podra parecernos que la definicin debe ser del tipo: const size_t sV = sizeof(Vector); Vector& operator[](int i) { return *( mVptr + (i * sV)); } sin embargo, reflexionando ms detenidamente recordamos que mVptr est definido precisamente como puntero-a-Vector, por lo que su lgebra lleva implcito el tamao de los objetos Vector ( 4.2.2), lo que significa que podemos prescindir del factor sV: Vector& operator[](int i) { return *( mVptr + i ); } Recordando la definicin de subndice 1a , y la relacin entre punteros y matrices ( 4.3.2) la expresin anterior equivale a: Vector& operator[](int i) { return mVptr[i]; } esta es justamente la definicin que utilizamos para la funcin-operador operator[ ] ( L.30) de nuestra clase. Como resultado, podemos utilizar expresiones del tipo: mVector mV1(5); // objeto tipo mVector (matriz de 5 Vectores) mV1[2]; // tercer elemento de la matriz
4 Operador de asignacin Para utilizar el operador de asignacin = con los objetos devueltos por el selector de miembro [ ], debemos sobrecargarlo para los objetos tipoVector. Esto se ha visto en el epgrafe correspondiente, por lo que nos limitamos a copiar dicha definicin ( 4.9.18a): Vector& operator= (const Vector& v) { // funcin operator= x = v.x; y = v.y; return *this; } Su implementacin en la versin definitiva ( L.6), nos permite utilizar expresiones del tipo: Vector v1; v1 = mV1[0]; // M.6:
5 Producto por un escalar Para mimetizar el comportamiento expresado en L.4 , sobrecargamos el operador producto para la clase Vector de forma que acepte un int. La definicin la hacemos de forma que corresponda a la definicin tradicional. Es decir, la resultante es un vector cuyos componentes son el producto de los componentes x y del vector operando por el escalar. Es importante observar aqu que, en el caso de la matriz de enteros m, las dos sentencias siguientes son equivalentes: m[i] = m[j] * 3; // producto por un escalar (por la derecha) m[i] = 3 * m[j]; // producto por un escalar (por la izquierda)
5.1 Esto significa que debemos definir el producto en ambos sentidos. Para el primero podemos definir una funcin miembro que acepte un argumento tipo int (adems del correspondiente puntero this). Este mtodo tiene el aspecto que se indica: Vector operator* (int i) { // producto por un escalar (por la derecha) Vector vr; vr.x = x * i; vr.y = y * i; return vr; } Despus de implementado en la versin definitiva ( L.9), nos permite expresiones del tipo: mV1[4] = mV1[0] * 5; // M.8:
5.2 El producto por la izquierda debemos definirlo como una funcin-operador externa. Se trata de una funcin independiente (no pertenece a una clase) que acepta dos argumentos, un int y un Vector. Como es usual, la declaramos friend de la clase Vector ( L.15) para que pueda tener acceso a sus miembros (aunque en este caso no es necesario porque todos son pblicos). Su diseo es muy parecido al anterior, aunque en este caso no existe puntero implcito this y debemos referenciar el objeto Vector directamente: Vector operator* (int i, Vector v) { Vector vr; vr.x = v.x * i; vr.y = v.y * i; return vr; } Su implementacin ( L.38) hace posible expresiones como: mV1[2] = 5 * mV1[0]; // M.11:
6 Una vez introducidas todas las modificaciones anteriores en la versin bsica , el diseo resultante es el siguiente: #include <iostream> using namespace std;
class Vector { // definicin de clase Vector public: int x, y; Vector& operator= (const Vector& v) { // L.6: asignacin V = V x = v.x; y = v.y; return *this; } Vector operator* (int i) { // L.9: Producto V * int Vector vr; vr.x = x * i; vr.y = y * i; return vr; } void showV(); friend Vector operator* (int, Vector); // L.15: Producto int * V }; void Vector::showV() { cout << "X = " << x << "; Y = " << y << endl; }
class mVector { // definicin de clase mVector int dimension; // L.20: public: Vector* mVptr; mVector(int n = 1) { // constructor por defecto dimension = n; mVptr = new Vector[dimension]; // L.25: } ~mVector() { // destructor delete [] mVptr; } Vector& operator[](int i) { return mVptr[i]; } void showmem (int); // L.31: };
mV1[2] = 5 * mV1[0]; // M.11: mV1.showmem(2); } Salida: X = 2; Y = 3 X = 2; Y = 3 X = 10; Y = 15 X = 10; Y = 15 Comentario En un programa real, se debera implementar un mecanismo de control de excepciones que pudiera controlar la posibilidad de que el operadornew del constructor (L.25) fuese incapaz de crear el objeto ( 4.9.20). Es decir, controlar que operaciones como la de M.1 concluyen con xito. Para manejar convenientemente los lmites incluimos en mVector el miembro dimension (L.20); su valor es iniciado por el constructor (L.24 ) y acompaa a cada instancia. El efecto es que es posible implementar la interfaz de la clase de forma que el usuario no pueda acceder un elemento fuera del espacio de la matriz. Para facilitar la lectura incluimos en la mVector el mtodo showmem (L.31) que muestra los componentes de un elemento de la matriz. Este mtodo utiliza el miembro dimension para verificar que no pretendemos acceder a un elemento fuera de los lmites del objeto previamente creado. 4.9.18e Sobrecarga del operador -> 1 Antecedentes El selector indirecto de miembro -> ( 4.9.16) es un operador binario [1] que permite acceder a miembros de objetos cuando se dispone de punteros a la clase correspondiente. Una expresin del tipo Cpt->membr representa el miembro de identificador membr de la clase Cl siempre que Cpt sea un puntero a dicha clase. Ejemplo: class Cl { public: int x; } c1, *ClPt = &c1; ... ClPt->x = 10;
Sabemos que la expresin ClPt->x exige que el primer operando Clpt sea un puntero a la clase, y que el segundo x, sea el identificador de uno de sus miembros. 2 Sinopsis La gramtica C++ permite definir una funcin miembro operator-> que puede ser invocada con la sintaxis del operador selector indirecto de miembro ->. Por ejemplo, siendo obj una instancia de la clase Cl para la que se define la funcin operator->, y membr un miembro de la misma [2], la expresin: obj->membr; es transformada por el compilador en una invocacin del tipo: ( obj.operator->() )->membr;
La parte entre parntesis obj.operator->(), representa la invocacin del mtodo operator- > sobre el objeto obj. Puesto que el valor devuelto por la funcin ser considerado a su vez el primer operando de -> aplicado a membr, la funcin operator-> debe devolver un puntero a un objeto de la clase sobre el que se pueda aplicar el operador ->. Es decir, su diseo debe tener el siguiente aspecto: class Vector { ... Vector* operator-> () { ... return this; } }; Observe que el puntero que se obtiene como resultado de la invocacin obj.operator->() no depende de la naturaleza del operando membr. Por esta razn se considera a veces que operator- > es un operador unario posfijo ( 4.9). Lo que significa que una expresin como v.operator->(); tiene sentido y devuelve un puntero al objeto: Vector v1; Vector* vptr; vptr = v1.operator->(); // Ok! vptr seala ahora a v1 3 Condiciones Para conseguir este comportamiento el compilador impone ciertas limitaciones, de forma que la funcin operator-> solo puede ser sobrecargada cumpliendo simultneamente las siguientes condiciones: a. Ser una funcin-miembro no esttica (que incluya el puntero this como argumento implcito 4.11.6). b. Ser una funcin-miembro que no acepte argumentos. Ejemplo: class Cl { ... friend Cl* operator->(); // Error debe ser una funcin-miembro Cl* operator->(int i) {/*...*/} // Error no acepta argumentos Cl* operator->() {/*...*/} // Ok. }; 4 El operador -> no puede ser sobrecargado Aunque esta afirmacin puede parecer escandalosa, ya que est en contradiccin con el ttulo del captulo. Y adems, en cualquier bibliografa que se consulte, la descripcin de la funcin operator-> se encuentra siempre en el captulo dedicado a la sobrecarga de operadores [3]. Sin embargo, no se trata de una verdadera sobrecarga del selector indirecto ->. Al menos, no en el sentido en que este mecanismo funciona con el resto de operadores. Observe que en realidad, el compilador se limita a sustituir el primer operando de la expresin obj->membr por la invocacin a una funcin-miembro, y una posterior utilizacin del resultado como primer operando de la versin global del operador, mientras el segundo operando se mantiene invariable. Adems, en dicha expresin (invocacin de la funcin-miembro), el primer operando debe ser necesariamente un objeto (instancia) de la clase y no un puntero Cl* como exige el uso regular del selector ->.
4.1 Este comportamiento, distinto de aquellos casos en que la versin global del operador es sustituida "realmente" por la versin sobrecargada, puede verificarse con un sencillo ejemplo: #include <iostream> using namespace std;
cout << ( ( v1 == v2 )? "Iguales" : "Distintos" ) << endl; // M.4 cout << "v1.x == " << vptr->x << endl; // M.5 } Salida: Invocada funcion operator==() Distintos v1.x == 2 Comentario Como puede verse, la utilizacin del operador de identidad == en M.4, provoca la utilizacin de la versin sobrecargada ( 4.9.18b1). As mismo, la ausencia de la definicin de este operador (L.6), habra producido un error de compilacin al tratar de utilizarlo en M.4: 'operator==' not implemented in type 'Vector' for arguments of the same type in ... Esto significa lisa y llanamente que el compilador no proporciona una versin por defecto de este operador (de identidad) para los objetos de la clase Vector. En cambio, la utilizacin de la (supuesta) versin sobrecargada del selector indirecto de miembro - > (M.5), no produce la invocacin automtica de la misma como ocurri en el caso de la identidad. En realidad, ante expresiones del tipo vptr->x como en M.5, el compilador sigue utilizando la versin global (por defecto) del operador.
4.2 Si en el ejemplo anterior sustituimos las lneas M.4/5 por: cout << "v1.x == " << v1->x << endl; // a cout << "v1.x == " << v1.operator->()->x << endl; // b cout << "v1.x == " << ( *v1.operator->() ).x << endl; // c La salida indica que estas tres formas s implican la invocacin de la funcin operator-> Invocada funcion operator->() v1.x == 2 Invocada funcion operator->() v1.x == 2 Invocada funcion operator->() v1.x == 2 Ya sabemos que le forma a es transformada por el compilador en la forma b, por lo que en realidad se trata de tres invocaciones explcitas a la funcin operator->. Observe que a, b y c son equivalentes, y representan variaciones sintcticas para referirse al elemento v1.x. 5 Punteros inteligentes Debemos resaltar que en el programa anterior disponemos de dos formas de acceso indirecto a los miembros del objeto v1: cout << v1->x; // a cout << vptr->x; // d
Hemos indicado que ambas utilizan la versin global del selector -> sobre el miembro x como segundo operando. Pero existe una diferencia crucial: la forma a permite introducir una funcin previa, representada por operator->( ), lo que abre todo un mundo de posibilidades. En realidad, este comportamiento atpico de la funcin operator->( ), que hemos visto se aparta del resto de operadores, no es arbitraria. Representa la puerta de acceso a lo que se denominan punteros inteligentes; objetos que actan como punteros, pero que adems pueden realizar alguna accin previa cada vez que un objeto es accedido a travs de ellos. Habida cuenta que esta accin previa puede ser cualquiera (todo lo que pueda hacer una funcin), los punteros inteligentes permiten tcnicas de programacin muy interesantes. La idea puede ser concretada en tres formas bsicas que comentamos separadamente: 1. Incluir la funcin operator-> en la definicin de la clase 2. Incluir la funcin operator-> en una clase independiente 3. Incluir la funcin oprator-> en una clase anidada
5.1 Incluir la funcin operator->( ) en la definicin de la clase (es el caso del ejemplo anterior): class Vector { public: int x, y; Vector* operator-> () { /* funcionalidad adicional requerida */ return this; } }; Como se ha visto, este diseo permite que los objetos de la clase Vector puedan ser accedidos indirectamente: Vector v1; v1->x = 2; v1->y = 4;
5.2 Incluir la funcin operator->( ) en una clase Vptr independiente: class Vector { ... };
En este caso los objetos Vptr pueden ser utilizados para acceder a los de clase Vector, de forma parecida a como se utilizan los punteros. Lo ilustramos con un ejemplo compilable: #include <iostream> using namespace std;
5.3 Incluir la funcin operator->( ) en una clase Vptr contenida (anidada) en Vector: class Vector { ... class Vptr { ... Vector* operator->() { /* funcionalidad adicional requerida */ return vpt; } }; };
Si los objetos Vptr no tienen sentido como entes independientes de los objetos Vector, o no pueden ser utilizados para otras clases, esta disposicin tambin puede ser vlida y en cierta forma equivalente al diseo anterior. Veamos este diseo en un ejemplo concreto: #include <iostream> using namespace std;
void main() { // ============== Vector v1 = {1, 2}; v1.p1.vpt = &v1; // M.2 cout << "v1.x == " << v1.p1->x << endl; // M.3 v1.p1->y = 4; cout << "v1.y == " << v1.p1->y << endl; } Como puede suponerse, la salida que se obtiene es idntica a la del ejemplo anterior. Observe en M.2 la inicializacin del puntero vpt; un miembro del objeto p1. (miembro a su vez del objeto v1). Esta circunstancia, miembros de objetos que pueden tener a su vez propiedades y mtodos, es caracterstica de las clases cuyos miembros son a su vez instancias de otras clases. En este caso, de la clase Vptr definida dentro de Vector. Observe tambin que en M.3 y siguientes se invoca el mtodo operator-> del miembro p1 del objeto v1.
5.3.2 Aunque la versin anterior es totalmente funcional, su diseo nos obliga a inicializar el puntero vpt para cada instancia de la clase Vector(como se ha hecho en M.2). En el ejemplo que sigue refinamos ligeramente el diseo anterior aadindole un constructor, al objeto de evitar tener que realizar esta inicializacin cada vez. #include <iostream> using namespace std;
class Vector { public: int x, y; class Vptr { public: Vector* vpt; Vector* operator-> () { cout << "Accedido vector (" << vpt->x << ", " << vpt->y << ")" << endl; return vpt; } } p1; Vector (int i = 0, int j = 0) { // L.14: constructor x = i; y = j; p1.vpt = this; } };
void main() { // ================ Vector v1 = Vector(2, 1); // M.1 // v1.p1.vpt = &v1; ya no es necesaria M.2 cout << "v1.x == " << v1.p1->x << endl; v1.p1->y = 4; cout << "v1.y == " << v1.p1->y << endl; } La salida es tambin idntica a la de los dos anteriores. El cambio introducido se limita casi exclusivamente a la inclusin de un constructor explcito (L.14) para la clase Vector. Como puede comprobarse, la inicializacin M.2 ya no es necesaria; se ha encomendado esta funcin al constructor. Ntese la utilizacin explcita del puntero this ( 4.11.6) para este cometido. Observe tambin que la existencia de un constructor explcito nos ha obligado a modificar ligeramente la sintaxis de la sentencia M.1 en la que creamos e inicializamos el objeto v1 ( 4.11.2d3). En cualquier caso, cualquiera que sea el diseo adoptado para la "sobrecarga" del selector indirecto, debemos conservar la idea central: estos punteros inteligentes permiten la utilizacin de un cdigo cuya ejecucin es previa al acceso al objeto. En los ejemplos anteriores dicho cdigo se ha concretado en una salida mostrando el estado actual (componentes) del vector. 4.9.18f Sobrecarga del operador de invocacin de funcin 1 Antecedentes: La invocacin de funciones ( 4.4.6) en C++ tiene la siguiente sintaxis general: expresin-postfija ( <lista-de-expresiones> );
1.1 En su utilizacin normal, expresin-postfija es el nombre de una funcin, un puntero a funcin o la referencia a una funcin. Por ejemplo: float sum(int i, int j) { float s = i + j; cout << "La suma es: " << s << endl; return s; } ... float (*fptr)(int, int) = sum; // definicin de puntero-a-funcin float (&fref)(int, int) = sum; // definicin de referencia-a-funcin int x = 2, y = 5; sum(x*2, y); // Ok. invocacin fptr(x*2, y); // Ok. invocacin fref(x*2, y); // Ok. invocacin
1.2 Cuando se utiliza con funciones-miembro (mtodos), expresin-postfija es el nombre de un mtodo, una expresin de puntero-a-clase (utilizado para seleccionar un mtodo), o de puntero-a-miembro. Por ejemplo: class Vector { // una clase cualquiera float x, y; public: void getm(int i) { // funcin-miembro (mtodo) cout << "Vector: (" << x * i << ", " << y * i << ") " << endl; } }; ... Vector v1; // Objeto Vector* vptr = &v1; // definicin de puntero-a-clase void (Vector::* vmptr) (int) // definicin de puntero-a-miembro = &Vector::getm; int x = 2; v1.getm(x); // Ok. invocacin del mtodo vptr->getm(x); // Ok. dem. (v1.*vmptr)(x); // Ok. dem.
En este contexto nos referimos al parntesis ( ) como operador de invocacin de funcin ( 4.9.16), aunque sabemos que tiene otros usos en el lenguaje: servir de signo de puntuacin ( 3.2.6) y delimitador en algunas expresiones. Por ejemplo, las expresiones con coma ( 4.10.5). 2 Sinopsis: La gramtica C++ permite definir una funcin-miembro no esttica operator( ) cuya definicin sea del tipo [1]: valor-devuelto operator( )( <lista-de-argumentos> ) { /* definicin */ } // 2a En este caso, las instancias de clases en que se han definido estos mtodos, presentan una curiosa peculiaridad sintctica: que sus mtodosoperator() pueden ser invocados utilizando directamente el identificador del objeto como expresin-postfija es decir: obj( <lista-de-argumentos> ); siendo obj una instancia de la clase Cl para la que se define la funcin operator( ). Por ejemplo: class Cl { public: ... void operator()(int x) { /* definicin */ } ... };
... Cl obj; obj(5); // Ok!! Cuando se utiliza esta notacin, el compilador transforma la expresin anterior en una invocacin a operator( ) en la forma cannica: obj.operator()( <lista-de-argumentos> ); // 2b Observe que se trata simplemente de la invocacin de una funcin-miembro sobre el objeto obj, y que nada impide que sea invocada directamente al modo tradicional con la sintaxis cannica 2b . Es decir, utilizando explcitamente la sustitucin realizada por el compilador. No confundir la expresin anterior (2a ) con la utilizacin de operator( ) como operador de conversin ( 4.9.18k), donde se utiliza sin especificacin del valor devuelto y sin que pueda aceptar ningn tipo de parmetro: operator( ){ /* valor devuelto */ } // 2c Recordemos que operator( ) puede aparecer con dos significados en el interior de una clase: class C { valor-devuelto operator()(argumentos); // operador de invocacin a funcin operator() { /* ... */ } // operador de conversin ... };
2.1 Lo ilustramos con un ejemplo: #include <iostream> using namespace std;
class Vector { public: float x, y; void operator()() { // funcin-operador cout << "Vector: (" << x << ", " << y << ") " << endl; } };
void main () { // ================= Vector v1 = {2, 3}; v1(); // Ok. invocacin de v1.operator() v1.operator()(); // Ok. invocacin clsica } Salida Vector: (2, 3) Vector: (2, 3) 3 El operador de invocacin de funcin no es sobrecargable Respecto a la "sobrecarga" del operador de invocacin de funcin ( ), podemos decir algo anlogo a lo indicado para la sobrecarga del selector indirecto ->; ( 4.9.18e): a pesar de que en la literatura sobre el tema, la descripcin de la funcin operator( ) se encuentra siempre en el captulo dedicado a la sobrecarga de operadores, en realidad no se trata de tal sobrecarga. Al menos no en un sentido homogneo al empleado con el resto de opradores. Hemos visto que se trata de una mera curiosidad sintctica; una forma algo extraa de invocacin de determinadas funciones-miembro (cuando estas funciones responden a un nombre especial). Al hilo de lo anterior, y dado que el identificador operator( ) es nico, resulta evidente que si se definen varias de estas funciones, se aplicar la congruencia estndar de argumentos ( 4.4.1a) para resolver cualquier ambigedad. Por ejemplo: #include <iostream> using namespace std;
void main () { // ============ Vector v1 = {2, 3}; v1(); // Ok. invoca versin-1 b v1(1); // Ok. invoca versin-2 c } Salida: Vector: (2, 3) Coordenadas: (2, 3) Comentario Observe que en L.9, el argumento (int) de la segunda definicin se ha utilizado exclusivamente para permitir al compilador distinguir entre ambas. Esta tcnica ya la hemos visto en la sobrecarga de los post-operadores incremento y decremento ( 4.9.18c). 4 Objetos-funcin Lo indicado hasta aqu podra parecer un mero capricho sintctico del creador del lenguaje; una forma particular de invocacin de ciertas funciones-miembro (de nombre especial), que presentan la singularidad de permitir utilizar objetos como si fuesen funciones (caso de las expresiones b y c ), pero que no tienen una justificacin objetiva, ya que no resuelve un problema que no pueda ser resuelto con alguno de los recursos existentes en el lenguaje. Precisamente, en razn de que pueden ser utilizadas como funciones, las instancias de clases para las que se han definido funciones operator( ), reciben indistintamente el nombre de objetos- funcin, funciones-objeto [2] o functor, y algn autor ha definido a estas entidades como "datos ejecutables" [3]. En realidad, como ocurre con otros detalles de su diseo, este aparente capricho sintctico encierra un mundo de sutilezas. Su importancia y razn de ser estriban en que permite escribir cdigo que realiza operaciones complejas a travs de argumentos de funciones ( 5.1.3a1). Precisamente la Librera Estndar de Plantillas C++ (STL 5.1) o sus extensiones, como las libreras Boost, donde se encuentran algunos de los conceptos y algoritmos ms sofisticados que haya construido hasta el momento la ingeniera de software, utiliza con profusin este tipo de recursos. Recuerde que los objetos (instancias de clases) pueden ser pasados como argumentos de funciones y que sus mtodos pueden acceder a las propiedades de la clase, de forma que pasar un objeto-funcin como argumento, equivale a pasar a la funcin ms informacin de la que supondra un escalar. Esta circunstancia tiene muchas aplicaciones. Por ejemplo, la nueva versin del Estndar permitir crear en una aplicacin un hilo ("thread") de ejecucin mediante una expresin del tipo: void work_to_do(); // L.1 std::thread newThread (work_to_do); // L.2 La funcin work_to_do() define el proceso que se ejecutar en el nuevo hilo representado por el objeto newThread de L.2. Sin embargo, el constructor de la clase std::trhead exige como argumento un puntero a funcin que no recibe argumentos y devuelve void. De forma que no es posible pasar en el constructor ninguna informacin adicional sobre detalles de la tarea a realizar y es en este punto, donde las caractersticas de C++ vienen al rescate, porque al igual que muchos otros algoritmos de la Librera Estndar C++, el argumento no tiene porqu ser necesariamente una funcin ordinaria; tambin puede ser un objeto-funcin, as que el diseo podra ser como sigue: class Work_to_do { public: // miembros que representan particularidades del proceso int a_, b_; // miembro que representa el resultado del proceso int& c_; // constructor que permite fijar las caractersticas Work_to_do (int a, int b, int& c) : a_(a), b_(b), c_(c) {}
// operador de invocacin a funcin void operator()() { c_ = a_ + b_; } }; ... int r; std::thread newThread (Work_to_do(1,2,r)); Observe que el argumento Work_to_do(x,y,z) es una llamada al constructor de la clase, que genera un objeto-funcin; que a su vez, es pasado al constructor de la clase std::trhead para construir el objeto newThread (que representa el nuevo hilo). Una vez concluida la tarea, el resultado lo obtenemos en r. 4.1 Unin de argumentos Observe que en el ejemplo anterior, el recurso ha consistido en empaquetar los argumentos involucrados en un objeto-funcin. Esta tcnica, conocida como unin o empaquetado de argumentos ("argument binding") es ampliamente utilizada, aunque tal como la hemos presentado, tiene el inconveniente de que hay que preparar manualmente la clase adecuada. Sin embargo, si como suele ser frecuente [4] la situacin se repite, es posible automatizarla utilizando una plantilla. Supongamos que el proceso que deba ejecutar la hebra ("thread") pueda ser definida genricamente mediante una funcin del tipo void work_to_do (A a, B b, C& c); En la que los objetos a y b representan los datos particulares y c la variable en la que se obtiene el resultado. En estas circunstancias, es posible definir una clase genrica ( 4.12.2) tal como: template <typename A, typename B, typename C> class bind { public: A a_; B b_; C& c_; void (*pf)(A,B,C&); bind (void (*p)(A,B,C&), A a, B b, C& c) : pf(p), a_(a), b_(b), c_(c) {}
void operator()() { pf(a_, b_, c_); } }; Suponiendo que tenemos definida la funcin que realiza el proceso en un caso concreto: void process1 (int a, char b, float& c) { /* proceso a realizar por la hebra el resultado es situado en c */ ... } Para lanzar una hebra que realizara esa tarea, solo tendramos que incluir un par de lenas en nuestro cdigo: float f; std:thread tread1 (bind (process1, 2, 'c', f));
4.9.18g Sobrecarga de operadores lgicos 1 Sinopsis: Recordemos ( 4.9.8) que los operadores lgicos suelen ser representados por sus nombres en ingls (maysculas): AND (&&); OR (||) y NOT (!). Los dos primeros son binarios, mientras que el ltimo es unario, lo que significa que AND y OR aceptan dos argumentos, mientras que la negacin NOT, acepta solo uno. Recordemos tambin que los operandos de las versiones globales de estos operadores son convertidos a un tipobool, y que el resultado es tambin un tipo bool de valor cierto/falso (true/false). Cualquier intento de aplicar estos operadores a tipos abstractos ( 2.2) genera un error de compilacin recordndonos que la operacin solo est definida para los tipos bsicos (preconstruidos en el lenguaje). La solucin en estos casos es sobrecargar adecuadamente estos operadores para los miembros de la clase, lo que puede realizarse mediante los procedimientos estndar ya sealados para operadores unarios ( 4.9.18c) y binarios ( 4.9.18b). 2 Permanencia de las leyes formales Antes de exponer algunos ejemplos, recordemos los preceptos que hemos denominado de permanencia de las leyes formales ( 4.9.8) que son especialmente pertinentes en estos casos de sobrecarga. Hemos sealado que la sobrecarga permite al programador una gran libertad, de forma que puede cambiar totalmente la funcionalidad del operador respecto a la que tiene con los tipos bsicos. Por ejemplo, podemos definir el operador AND entre miembros c1 y c2 de una clase C de forma que devuelva un valor de cualquier tipo en vez de un booleano, aunque lo lgico sera que fuese un bool, con objeto que el resultado de estas operaciones fuese el que se espera intuitivamente. Adems de esto, considere que los tres operadores estn relacionados desde el punto de vista lgico, y deberan seguir estndolo de la misma forma en las versiones sobrecargadas. Por ejemplo, si definimos la sobrecarga del operador de negacin lgica NOT de forma que para los objetos c1 y c2 resulta: !c1 == false !c2 == false Deberamos definir la sobrecarga del operador AND de forma que (c1 && c2) == true De lo contrario estaramos construyendo para los objetos de tipo C una lgica bastante difcil de comprender a una mente acostumbrada al razonamiento estndar con los tipos bsicos del lenguaje. En este captulo trataremos de demostrar que si se quiere mantener una lgica coherente, la sobrecarga de los tres operadores lgicos AND, OR y NOT para tipos de una clase C, puede ser sustituida por una conversin de usuario mediante una funcin de conversin operator bool() adecuada ( 4.9.18k). 3 Sobrecarga del operador NOT El operador NOT de negacin lgica ( ! ) est relacionado con su contrario (que no tiene nombre ni representacin). Para explicar el significado de esta afirmacin, supongamos un objeto c de una clase C. Segn hemos sealado, un intento de utilizarlo, por ejemplo, la expresin: if (!c) { /* ... */ } genera un error de compilacin: 'operator!' not implemented in type 'C'..., en el que se indica que el operador NOT de negacin lgica no est definido para objetos de la clase. Sin embargo, podemos observar que un intento anlogo sin el operador: if (c) { /* ... */ } tambin produce un error, aunque en este caso la indicacin es ms ambiga: Illegal structure operation in function...(Borland) o: conditional expression of type 'class C' is illegal (Visual C++). En este ltimo caso el compilador est indicando que no sabe como convertir la expresin entre parntesis (c) a un tipo bool. Recuerde que la sentencia if(<condicion>)... espera recibir una expresin <condicion> que se resuelva en un bool ( 4.10.2).
3.1 Ambos inconvenientes pueden resolverse adoptando las medidas pertinentes. El primero sobrecargando el operador NOT para objetos de la clase. El segundo proporcionando una conversin de usuario que permita al compilador transformar un tipo C en un bool ( 4.9.18k). Por ejemplo, supongamos una clase V2D para contener puntos de un plano definidos por sus coordenadas cartesianas. Para ciertas operaciones lgicas con estos objetos, consideramos "ciertos" los puntos que pertenecen al primer cuadrante y "falsos" todos los dems. Un posible diseo sera el siguiente: class V2D { // clase de puntos en el plano float x, y; public: V2D(float i=0, float j=0): x(i), y(j) {} // constructor bool operator!() { // sobrecarga del operador NOT return ((x > 0 && y > 0) ? false : true ); } operator bool() { // conversin de usuario return ((x > 0 && y > 0) ? true : false ); } }; ... void func () { V2D p1(0,2); V2D p2(-1.1, 2); V2D p3 = Vector2D(1,2); V2D p4(1, -3);
if (p1) cout << "p1 Ok."; if (p2) cout << "p2 Ok."; if (p3) cout << "p3 Ok."; if (p4) cout << "p4 Ok."; // p3 Ok.
if (!p1) cout << "p1 Not Ok."; // p1 Not Ok. if (!p2) cout << "p2 Not Ok."; // p2 Not Ok. if (!p3) cout << "p3 Not Ok."; // p3 Not Ok. if (!p4) cout << "p4 Not Ok."; } Observe que la definicin de operator bool debe ser congruente con la definicin de operator!, de forma que un objeto no pueda ser cierto y falso al mismo tiempo. 3.2 Observe tambin que una vez definida la funcin de conversin operator bool, la de negacin no es realmente necesaria. En efecto: la definicin class V2D { // clase de puntos en el plano float x, y; public: V2D(float i=0, float j=0): x(i), y(j) {} // constructor operator bool() { // conversin de usuario return ((x > 0 && y > 0) ? true : false ); } }; produce exactamente las mismas salidas que la anterior. La razn es que en las expresiones (! p) el objeto p es convertido a tipo bool por accin de la versin global del propio operador NOT. Esta conversin se realiza mediante una invocacin del tipo p.operator bool().
3.3 Un ltimo truco podra permitirnos la operatoria inversa: obtener los valores (p) mediante una doble negacin (!!p). En cuyo caso podramos eliminar la conversin de usuario operator bool, dejando la sobrecarga del operador de negacin operator!: class V2D { // clase de puntos en el plano float x, y; public: V2D(float i=0, float j=0): x(i), y(j) {} // constructor bool operator!() { // sobrecarga del operador NOT return ((x > 0 && y > 0) ? false : true ); } }; ... void func () { V2D p1(0,2); V2D p2(-1.1, 2); V2D p3 = Vector2D(1,2); V2D p4(1, -3);
if (!!p1) cout << "p1 Ok."; if (!!p2) cout << "p2 Ok."; if (!!p3) cout << "p3 Ok."; if (!!p4) cout << "p4 Ok."; // p3 Ok.
if (!p1) cout << "p1 Not Ok."; // p1 Not Ok. if (!p2) cout << "p2 Not Ok."; // p2 Not Ok. if (!p3) cout << "p3 Not Ok."; // p3 Not Ok. if (!p4) cout << "p4 Not Ok."; }
4 Sobrecarga de peradores AND y OR Es significativo que si, como en el caso anterior 3.2 , se dispone de una conversin de usuario que garantice la conversin de un objeto c de tipo C a tipo bool, entonces no es realmente necesario sobrecargar los operadores AND ni OR para poder utilizarlos. Por ejemplo, suponiendo la definicin (3.2) y los vectores (3.3) anteriores, las sentencias: if (p3 && p1) cout << "p3 y p1 Ok."; else cout << "p3 y p1 NOT ok."; if (p3 || p1) cout << "p3 o p1 Ok."; else cout << "p3 o p1 NOT Ok."; Producen las siguientes salidas: p3 y p1 NOT ok. p3 o p1 Ok. La razn es la sealada en el caso del operador NOT (3.2 ). Si existe posibilidad de conversin de los operandos a tipos bool, entonces el compilador utiliza las versiones globales de ambos operadores una vez realizada la conversin correspondiente.
4.1 Si de todos modos es preciso sobrecargar alguno de estos operadores, el procedimiento es el mismo que con cualquier operador binario ( 4.9.18b): a. Declarando una funcin miembro no esttica que acepte un argumento b. Declarando una funcin externa (generalmente friend) que acepte dos argumentos.
Como ejemplo, procederemos a la sobrecarga de los operadores AND y OR para la clase V2D ya mencionada, que utilizamos para contener puntos de un plano. Las operaciones mantendrn coherencia con los principios utilizados al sobrecargar el operador NOT (3.1 ). Recordemos que para las operaciones lgicas con estos objetos, consideramos "ciertos" los puntos que pertenecen al primer cuadrante y "falsos" todos los dems. La sobrecarga de AND se realiza mediante el procedimiento a (una funcin miembro no esttica); para OR se utiliza el procedimiento b (una funcin externa que acepte dos argumentos). El diseo es el siguiente: #include <iostream> using namespace std;
if (p3 && p1) cout << "p3 y p1 Ok.\n"; else cout << "p3 y p1 NOT ok.\n"; if (p3 || p1) cout << "p3 o p1 Ok.\n"; else cout << "p3 o p1 NOT Ok.\n"; } Salida: p3 y p1 NOT ok. p3 o p1 Ok. Comentario Observe que las funciones operator&& y operator|| utilizan la versin global del operador AND (&&). Observe tambin que en ambas definiciones se ha respetado la mecnica descrita en el Estndar para las versiones globales. De forma que se devuelve un resultado tan pronto como se tiene constancia de este, sin necesidad de terminar todas las comprobaciones que puedan estar involucradas. Por ejemplo, en la definicin deoperator||, si se cumple la primera comprobacin (p1.x > 0 && p1.y > 0), se devuelve directamente un resultado true, sin necesidad de esperar a realizar la segunda. 4.9.18h Sobrecarga de Enumeraciones 1 Sinopsis El hecho de referirnos a la sobrecarga de enumeraciones ( 4.7) podra parecer en contradiccin con lo indicado al tratar de la sobrecarga en general ( 4.9.18): que "se refiere y tiene aplicacin solo cuando los operandos son instancias de clases". Sin embargo, hay que tener en cuenta que las enumeraciones son en realidad un tipo muy particular de estructuras (y por ende, de clases), que como tales, gozan de muchas de las caractersticas de aquellas [1]. 2 Sobrecarga Es posible sobrecargar la mayora de los operadores para una enumeracin, pero dado que estas no pueden tener funciones-miembro, no es posible sobrecargar aquellos que precisamente exigen ser sobrecargados a travs de mtodos no estticos. Concretamente los operadores =, [ ], ( ) y - > no pueden ser sobrecargados para un enum. La consecuencia inmediata es aadir que la sobrecarga de enumeraciones debe realizarse a travs de funciones ordinarias (no pertenecientes a clases), y que al menos uno de los argumentos debe ser del tipo de la enumeracin que se sobrecarga.
3 Comprobar en el ejemplo que sigue la forma de sobrecargar los operadores de preincremento y postincremento. Observe que en cierta forma, la enumeracin se comporta como una clase. #include <iostream> #using amespace std;
enum ESTACION { primavera, verano, otono, invierno }; ESTACION& operator++ (ESTACION& s) { // Preincremento ++@ s = ESTACION( (s + 1) % 4 ); // L.6 return s; } ESTACION operator++(ESTACION& s, int) { // Postincremento @++ ESTACION tmp = s; switch (s) { case primavera: s = verano; break; case verano: s = otono; break; case otono: s = invierno; break; case invierno: s = primavera; break; } return tmp; }
int main(void) { // ============= ESTACION est = otono; cout << "La estacion es " << est << endl; cout << "Preincrementar la estacion: "<< ++est << endl; cout << "No cambiar si se usa el postincremento: " << est++ << endl; cout << "Finalmente: " << est << endl; } Salida: La estacion es 2 Preincrementar la estacion: 3 No cambiar si se usa el postincremento: 3 Finalmente: 0 Comentario Por tratarse de la sobrecarga de operadores unarios como funciones externas, ambas funciones- operador se han definido utilizando la forma que hemos denominado "b" ( 4.9.18c): Preincremento: funcin que acepta un argumento operator++(ESTACION) Posincremento: funcin acepta un objeto y un entero operator++(ESTACION, int) El preincremento recibe el argumento por referencia (L.5), de forma que modifica el valor recibido. Observe como en L.6 se efecta el calculo aritmtico, seguido de un modelado al tipo ESTACION, y como en L.7 este valor es devuelto por referencia. Observe como el postincremento devuelve un objeto "por valor", y que este objeto es una copia del objeto inicial. Simultneamente la funcin modifica el valor del objeto recibido inicialmente (por referencia). 4.9.18k Conversiones definidas por el usuario El presente captulo es un claro ejemplo de un tpico difcil de clasificar. Aunque podra haber encajado igualmente bien (o mal) en otros sitios, lo hemos incluido en el epgrafe dedicado a la sobrecarga de operadores porque en una de sus formas se refiere a la funcin-operador operator. Este es tambin el criterio del Dr. Stroustrup en su obra TC++PL. 1 Prembulo Recordemos que el lenguaje C++ dispone de una serie de mecanismos de conversin para los tipos bsicos, que son utilizados automticamente en determinadas circunstancias. Son las conversiones estndar ( 2.2.5). El lenguaje tambin permite que puedan realizarse conversiones implcitas o explcitas para los tipos abstractos, aunque en este caso es el programador el que debe adoptar las medidas pertinentes, razn por la cual se denominan conversiones definidas por el usuario ("User-defined conversions"). Existen dos formas de definir estas ltimas conversiones: mediante constructores y mediante operadores de conversin. Ambos tipos sern tratados en el presente captulo. 2 Conversiones de constructor Hemos indicado ( 4.9.9) que el modelado de tipos est estrechamente relacionado con los constructores de clases, y que la posibilidad de realizar un modelado de un objeto b de tipo B, a otro tipo A distinto: a = A(b); depende de cmo est definido el constructor de la clase A. Para que esta conversin sea posible, debe existir un constructor de conversin ( 4.11.2d1) que acepte un objeto tipo B como nico argumento. Es decir, debe existir un mtodo: A::A(B b) { /* detalle de la conversin */ }
En realidad, los constructores de conversin constituyen el soporte del mecanismo C++ de modelado, de forma que la existencia de estos constructores es condicin necesaria y suficiente para que pueda efectuarse este ltimo (el modelado). Por ejemplo: class X { public: X(int); // constructor C-1 }; la mera existencia del constructor C-1 en la clase X, permite las siguientes asignaciones: void f() { X a = 1; // Ok. invocacin implcita a X(1) X b(1); // Ok. invocacin implcita a X(1) a = 2; // Ok. invocacin implcita a X(2) a = (X) 2; // Ok. casting explcito (estlo tradicional) a = static_cast<X>(2); // Ok. casting explcito (estilo C++) } Si eliminamos el constructor C-1 de la declaracin de la clase, las sentencias anteriores seran errneas. Nota: las tres ltimas sentencias implican en realidad dos operaciones: la creacin de un objeto temporal tipoX conteniendo el Rvalue de la expresin y una asignacin posterior utilizando el operador de asignacin implcito de la clase X. 2.1 Ejemplo Para ilustrar la problemtica de este tipo de conversiones explcitas e implcitas, construiremos un ejemplo ejecutable en el que creamos una clasePar que destinaremos a albergar los enteros mltiplos de 2. Lo que pretendemos es poder utilizar los miembros de esta clase en todas las circunstancias en que se podra utilizar un tipo bsico int suponiendo que su valor sea un nmero par (divisible por 2). El criterio para aceptar que un entero n puede ser miembro de la clase es que el resto de la divisin n/2 sea cero (consideramos que el cero es par, puesto que 0/2 es 0). El diseo bsico es el siguiente: #include <iostream> using namespace std;
class BadNumber { int num; // miembro -privado por defecto- public: BadNumber(int n=0): num(n) {} // constructor explcito int what() { return num; } // mtodo };
class Par { int val; void verify(int n) { if (n % 2) throw BadNumber(n); } public: Par(int n=0) { // L.15: constructor de conversin verify(n); val = n; cout << "Creado numero " << val << endl; } };
int main() { // ================= try { Par p0; // M2: Creado numero 0 Par p2 = 2; // M3: Creado numero 2 Par p3 = 3; // M4: Error: numero 3 impar Par p4 = (Par) 4; // M5: Creado numero 4 Par P6 = Par(6); // M6: Creado numero 6 } catch (BadNumber& e) { cout << "Error: numero " << e.what() << " impar." << endl; } } Salida: Las salidas producidas por las sentencias de asignacin contenidas en la funcin main, se han incluido en los comentarios junto a las sentencias, aunque las repetimos aqu: Creado numero 0 Creado numero 2 Error: numero 3 impar Creado numero 4 Creado numero 6 Comentario La clase BadNumber sirve para lanzar una excepcin en caso que se pretenda crear un nmero Par invlido. El mtodo what nos devuelve dicho nmero. La clase Par tiene un diseo muy simple: el miembro val almacena el nmero correspondiente. El mtodo verify sirve para comprobar que el "valor" del objeto a crear cumple la condicin exigida de ser par. En caso contrario lanza una excepcin ( 1.6) que ser recogida por el dispositivo try ... catch correspondiente de la funcin main. Se ha dotado a la clase de un constructor por defecto que acepta un int como argumento. Si el valor n suministrado pasa la verificacin correspondiente, se inicia el miembro val con el valor del argumento n y se muestra en pantalla el nmero creado. En los comentarios de las sentencias M2 a M6 se muestra la salida obtenida en cada caso. Puede comprobarse que la existencia de un constructor de conversin como el definido en L.15 permite los modelados implcitos en las sentencias M3 y M4 o explcitos (sentencias M5 y M6), donde las constantes numricas utilizadas como Rvalues son convertidas a objetos de tipo Par. Observe que no ha sido necesario definir el operador de asignacin = entre objetos Par. La versin por defecto suministrada por el compilador ( 4.9.18a) resulta suficiente para las asignaciones planteadas. 2.1 Aumentar la funcionalidad La clase diseada cumple con su funcin de almacenar nmero pares. Tambin permite una asignacin de tipo Par = int, y detecta cualquier intento de crear un objeto no vlido. Sin embargo, dista mucho de poder ser utilizada con la misma generalidad que los objetos de tipo int. Por ejemplo, las siguientes sentencias produciran un error de compilacin: Par p3 = p2 + 2; // Error Par p4 = p2 + p2; // Error Par p8 = 4 + p4; // Error La razn es que no estn definidas las operaciones correspondientes: suma Par & int; suma Par & Par, y suma int & Par [1]. Para acercar nuestro diseo a la funcionalidad deseada agregamos los operadores correspondientes. Seran los siguientes algoritmos: Nota: en los ejemplos que siguen, pi es un objeto tipo Par, y n es un int. 2.1a Operador suma + entre tipos Par e int (resuelve situaciones del tipo pi + n): operator+(int n) { verify(n); // verificar que el operando n es adecuado val += n; }
2.1b Operador suma + para tipos Par (resuelve situaciones del tipo pi + pj): Par operator+(const Par& p) { return Par(val + p.val); } Observe que el valor a devolver por la funcin se ha obtenido mediante una explcita al constructor utilizando el argumento adecuado. Aunque esta operacin no precisa de verificacin, de todas formas el constructor la realiza.
2.1c Operador suma + entre tipos int y Par (resuelve situaciones del tipo n + pi): Par operator+(int n, Par p) { Par pt(n); // L1: return pt + p; // L2: } La sentencia L1 crea un objeto automtico pt tipo Par y valor n, mediante una invocacin implcita al constructor de la clase (el constructor se encarga de verificar que el valor n es correcto). En L2 se utiliza el operador suma entre objetos tipo Par, definido en el punto anterior (2.1b ), para devolver el objeto resultante de la operacin. Una definicin alternativa y ms eficiente, sera la siguiente: Par operator+(int n, Par p) { return Par(p.val + n); }
Las dos primeras funciones-operador se integran como mtodos no estticos de clase; la ltima como funcin externa. En el listado adjunto se muestra el diseo resultante despus de las adiciones anteriores ( Listado-1). El nuevo diseo permite realizar las operaciones deseadas (2.1b ) con los resultados que se indican: Par p3 = p2 + 1; // -> Error: numero 1 impar. Par p4 = p2 + p2; // -> Creado numero 4 Par p5 = 3 + p2; // -> Error: numero 3 impar. Par p8 = 4 + p4; // -> Creado numero 4 // -> Creado numero 8 Observe que los resultados son los esperados; la doble salida generada por la ltima sentencia es producida por el algoritmo 2.1c . La primera corresponde a la creacin del objeto automtico pt. La segunda a la construccin del objeto a devolver realizada en L2. 2.2 Nuevas dificultades No obstante lo anterior, una total libertad para la utilizacin conjunta de nuestro tipo Par con el tipo int exigira muchas ms posibilidades. Por ejemplo: Par p2 += 2; Par p4 += p2; Par p8 /= 2; ++p2; etc. A las anteriores habra que aadir todas las circunstancias en que la conversin deba realizarse en sentido contrario (del tipo Par a int). Por ejemplo: int x = p2; int y = 3 + p2; y += p2; etc. Con el diseo actual de la clase Par, todas estas sentencias producen un error de compilacin. Su utilizacin exigira implementar toda una serie de versiones sobrecargadas de los operadores correspondientes, lo que supone desde luego una buena cantidad de cdigo.