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: 884