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]


4.9.18c  Sobrecarga de operadores unarios

§1  Sinopsis

Recordemos que los operadores unarios susceptibles de sobrecarga son:

  • Operadores unarios + y -  ( 4.9.1)

  • Operadores unarios de incremento ++ y decremento --  ( 4.9.1)

  • Operadores de puntero:  referencia &   ( 4.9.11) e indirección *  ( 4.9.11)

  • Operador de manejo de bits ("bitwise") complemento a uno ~ ( 4.9.3)

  • Operador de negación lógica  !  ( 4.9.8)

  • Asignación y desasignación dinámica de memoria: new  ( 4.9.20) y delete  ( 4.9.21)

§2  Sobrecarga de operadores ++ y --

Los operadores incremento ++ y decremento -- se sobrecargan de forma distinta según se trate de los "pre" o "post" operadores. Es decir, suponiendo que @ representa uno de ellos, la sobrecarga es distinta según se trate de la expresión @x o x@.


§2.1
  En el caso de los preoperadores ++/--, la sobrecarga para los miembros de una clase C puede hacerse de dos formas (ver a continuación el caso de los postoperadores):

2.1a.  Declarando una función-miembro no estática, que no acepte argumentos [2] del tipo:

C& C::operator++();

2.1b.  Declarando una función no miembro (generalmente friend) que acepte un argumento.

C& operator++(C& c);

Según lo anterior, y dependiendo de la declaración, la expresión @x puede ser interpretada como cualquiera de las dos formas:

2.1ax.operator@()

2.1boperator@(x)

Si han sido declaradas ambas formas, se aplica la congruencia estándar de argumentos ( 4.4.1a) para resolver cualquier posible ambigüedad.


§2.1.1  Sobrecarga del operador preincremento ++X

Para ilustrar la técnica a seguir y los problemas derivados de la sobrecarga de operadores unarios, construiremos un ejemplo muy sencillo en el que sobrecargamos el operador preincremento ++ para los miembros de una clase Entero, pero antes debemos recordar que este operador combina las propiedades de la asignación y de la suma. En efecto:  ++x puede ser expresado como x = x + 1.


§2.1.1a  Para repasar de forma gráfica este comportamiento consideremos el siguiente ejemplo:

int x = 5, y       // L.1:
y = ++x;           // L.2:
cout << "x == " << x << "; y == " << y << endl;

Salida:

x == 6; y == 6

La explicación del proceso es la siguiente: en L.1 se declaran dos variables tipo int y se inicializa una de ellas, la otra contiene basura ( 4.1.2). La sentencia L.2 se ejecuta de derecha a izquierda:

  1. El valor 5 de x es incrementado, pasando a valer 6
  2. El valor basura de y es sustituido por el valor 6 de x.


§2.1.1b  Naturalmente podemos sobrecargar el operador preincremento de forma que su comportamiento con miembros de la clase Entero sea totalmente distinta que cuando se aplica a tipos int, pero aquí lo haremos de forma que acepte el mismo álgebra que cuando se aplica a tipos simples ( 4.9.18). La única diferencia es que, en este caso, el operador ++ duplicará el valor del operando en vez de incrementarlo en una unidad. Para ello utilizamos un diseño similar al utilizado para sobrecargar el operador de asignación:

#include <iostream>
using namespace std;

class Entero {
  public: int x;
  Entero operator++ () { // L.6: función-operador operator++
    x = x + x;
    return *this;
  }
};

void main () {           // ========= (Comprobación)
  Entero e1 = {5}, e2;
  e2 = ++e1;             // M.2 uso de operador ++ sobrecargado
  cout << "e1 == " << e1.x << endl;
  cout << "e2 == " << e2.x << endl;
}

Salida:

e1 == 10
e2 == 10

Podemos comprobar como el resultado es el esperado, y que en M.2 es posible utilizar el operador preincremento con objetos tipo Entero.

Para asegurarnos de su idoneidad, extendamos el ejemplo de los enteros a una asignación encadenada:

int x = 5, y, z;       // L.1:
z = ++y = ++x;         // L.2:
cout << "x == " << x << "; y == " << y << "; z == " << z << endl;

Salida:

x == 6; y == 6; z == 6

Este caso es análogo al anterior . En L.1 se declaran tres variables tipo int y se inicializa una de ellas, las demás contienen basura. La sentencia L.2 se ejecuta de derecha a izquierda según la siguiente secuencia:

  1. El valor 5 de x es incrementado, pasando a valer 6
  2. El valor basura de y es incrementado, pasando a ser ??+1
  3. El valor 6 de x es asignado a y.
  4. El valor basura de z es sustituido por el valor 6 de y.

Hemos señalado que este comportamiento básico debe mantenerse en la versión sobrecargada para "Enteros", lo que comprobamos mediante una modificación en el cuerpo de la función main anterior , que pasa a tener el siguiente diseño:

Entero e1 = {5} , e2, e3;
e3 = ++e2 = ++e1;       // M.2:
cout << "e1 = " << e1.x << endl;
cout << "e2 = " << e2.x << endl;
cout << "e3 = " << e3.x << endl;

Salida:

e1 = 10
e2 = -1717986920
e3 = 10

Aunque no se obtiene ningún error con los tres compiladores utilizados, evidentemente la primera asignación explícita tiene problemas. Para averiguar su causa, modificamos la línea M.2:

++e2 = ++e1;         // M.2bis

Ahora si obtenemos un error [1]:  ... Lvalue required in function main(). Esta advertencia nos da la pista de la naturaleza del problema. En efecto, sabemos ( 2.1) que en una asignación, el Rvalue es un "valor", mientras que el Lvalue tiene el carácter de una "dirección". El diseño adoptado proporciona un Rvalue como resultado, cosa que hemos comprobado en la expresión M.3 del ejemplo donde la expresión e2 = ++e1; produce el resultado apetecido (++e1 produce un Rvalue). Pero como tal, no es válido como valor a la izquierda de una asignación. Es decir: ++e2 no puede estar a la izquierda, no es un Lvalue.

Podría pensarse que una solución sería modificar el diseño de Entero de forma que devuelva una dirección, es decir:

class Entero {
  public: int x;
  Entero* operator++ () {
    x = x + x;
    return this;
  }
};

Sin embargo, aunque la definición responde adecuadamente a la expresión ++e1, se comprueba que es inadecuada en los otros casos:

e2 = ++e1;     // L.1 Error: "cannot convert 'Entero*' to 'Entero'"
++e2 = e1;     // L.2 Error: "Lvalue required"


El error L.1 se debe al intento de asignación de un tipo puntero-a-Entero (Entero*) a un tipo Entero (e2). En cuanto al segundo, aunque en L.2 el resultado de ++e2 es un puntero, a fin de cuentas es un Rvalue (el hecho de que el "valor" sea la dirección de un objeto no menoscaba su condición de tal).

Puesto que no existe solución al problema con los recursos del C clásico, C++ introdujo el concepto de referencia ( 4.2.3), así que en realidad las referencias C++ son la solución de un problema muy específico. Se necesitaba un tipo de objetos con una doble personalidad: que se comportaran como Rvalue cuando estuviesen a la derecha de una asignación y como Lvalue en caso de estar a la izquierda [3].

Para verlo con más claridad hagamos un inciso y consideremos el siguiente código:

int& max (int& a, int& b) { return (a >= b)? a : b;}
...
int x = 10, y = 30, z;
z = max(x, y)               // L.4: z == 30
max(x, y) = 40;             // L.5: y == 40

La referencia devuelta en L.4 por la función actúa como un Rvalue; su valor es 30. Esta sentencia equivale a z = 30;. En L.5 el valor devuelto por la función se comporta como Lvalue, y su valor es la dirección de y; equivale a y = 40;. Este comportamiento, incluso cuando se trata de valores devueltos por una función, es exclusivo y único de las referencias y constituye la verdadera razón de su existencia en el lenguaje.

Volviendo a nuestro caso, la consecuencia es que la función operator++ debe devolver una referencia al objeto:

class Entero {
  public: int x;
  Entero& operator++ () {
    x = x + x;
    return *this;
  }
};

y las salidas proporcionadas por las expresiones de verificación son las correctas:

e1 = 10
e2 = 10
e3 = 10


§2.1.2  Sobrecarga del operador predecremento --@

Como ejemplo incluimos una versión 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 función-operador externa que acepte un argumento.

Nota: aunque no es necesario, porque la única propiedad de la clase es pública, 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 función.

#include <iostream>
using namespace std;

class Entero {
  public: int x;
  friend Entero& operator++(Entero&);
  friend Entero& operator--(Entero&);
};
Entero& operator++ (Entero& e) {
  e.x = e.x + e.x;
  return e;
}
Entero& operator-- (Entero& e) {
  e.x = e.x / 2;
  return e;
}

void main () {      // ==============
  Entero e1, e2, e3;
  e1.x = 5;
  e3 = ++e2 = ++e1;
  cout << " e1 = " << e1.x << "; e2 = " << e2.x
       << "; e3 = " << e3.x << endl;
  e3 = --e2 = --e1;
  cout << " e1 = " << e1.x << "; e2 = " << e2.x
       << "; e3 = " << e3.x << endl;
}

Salida:

e1 = 10; e2 = 10; e3 = 10
e1 = 5; e2 = 5; e3 = 5


§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 función miembro no estática que acepte un entero como argumento.  Ejemplo:

C C::operator++(int); 

2.2b.  Declarando una función no miembro (generalmente friend) que acepte un objeto de la clase y un entero como argumentos (en este orden).  Ejemplo:

C operator-- (C&, int);

Según lo anterior, y dependiendo de la declaración, si @ representa un post-operador unitario (++ o --), la expresión 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 inclusión del entero como argumento es simplemente un recurso de los diseñadores del lenguaje para que el compilador pueda distinguir las definiciones de los "pre" y "post" operadores ++ y -- (el argumento no se usa para nada más). De hecho, las primeras versiones C++ (hasta la versión 2.0 del preprocesador cfront de AT&T),  no distinguían 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 lógica 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 definición del primero utilizamos la solución 2.2a, y la 2.2b para el segundo.

#include <iostream>
using namespace std;

class Entero {
  public: int x;
  friend Entero& operator++(Entero&);     // L.6: preincremento
  friend Entero& operator--(Entero&);     // L.7: predecremento
  Entero operator++(int i) {              // L.8: postincremento
    Entero tmp = *this;                   // L.9:
    x = x + x;   return tmp;
  }
  friend Entero operator--(Entero&, int); // L.12: postdecremento
};

Entero& operator++ (Entero& e) {          // preincremento
   e.x = e.x + e.x;  return e;
}
Entero& operator-- (Entero& e) {          // predecremento
   e.x = e.x / 2;  return e;
}
Entero operator-- (Entero& e, int i) {    // L.21: postdecremento
  Entero tmp = e;                         // L.22:
  e.x = e.x / 2;  return tmp;
}

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 definición de los operadores preincremento (L.6) y predecremento (L.7) se ha utilizado la fórmula 2.1b : "declarar una función no miembro que acepte un argumento". Para postincremento (L.8), se ha utilizado la opción 2.2a "función miembro no estática que acepte un entero como argumento". Finalmente, para el posdecremento (L.12) se ha utilizado la opción 2.2b "una función 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.x tiene el valor 6). A continuación 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 continuación e1 es decrementado, con lo que el valor final de e1.x es 6; los resultado se ven en la segunda salida.

Es interesante observar que los operadores "post" incremento/decremento presentan una dificultad teórica al ser tratados como funciones: se precisa un mecanismo que aplique el resultado exigido, pero devuelva el objeto en su estado "anterior" a la aplicación 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 explícitamente como argumento.

Este objeto temporal tmp es el que realmente se devuelve, con lo que la función-operador devuelve un objeto análogo al operando antes de la modificación, al mismo tiempo que las modifcaciones se realizan sobre el objeto original. En nuestro caso, la modificación de la propiedad x del operando, se realiza como:

x = x + x;        // L.10:

cuando se trata de una función-miembro, y como:

e.x = e.x / 2;    // L.23:

cuando la función-operador no es miembro de la clase.

Nótese que en este último caso, el objeto pasado como argumento debe serlo como referencia (L.12 y L.21). La razón es que la función-operador debe modificar el valor del objeto pasado como argumento ( 4.2.3).

Observe también que este diseño 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 función se trata a través del análisis de sus argumentos.

Nota: el diseño de los operadores "post" presenta una importante dificultad teórica: 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 asignación (como un Lvalue). Es decir, no son posibles asignaciones del tipo:

e2++ = e1;         // Error: "Lvalue required"

por tanto tampoco son posibles expresiones de asignación compuesta del tipo:

e3 = e2++ = e1++;  // Error:

que sí son posibles con los pre-operadores . Esta limitación es extensiva incluso a los tipos básicos; 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 análogas con preincremento y predecremento sí son posibles :

z = ++y = ++x;   // Ok.


  En la página adjunta se expone un ejemplo de una técnica que utiliza la indirección ( 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 indirección

Recordemos que el operador de indirección * ( 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 página adjunta se incluye un completo ejemplo de su utilización ( Ejemplo).


§4  Sobrecarga del operador de negación lógica

La sobrecarga del operador !  NOT de negación lógica puede verse en el epígrafe ( 4.9.18g) junto con las sobrecargas del resto de operadores lógicos (binarios).

  Inicio.


[1]  En este punto difiere el comportamiento de los compiladores: Borland C++ 5.5 muestra el error señalado, mientras que Visual C++ 6.0 y Linux GCC v 2.95.3 compilan sin problemas, produciendo una salida errónea como en el caso anterior. Hemos incluido el primero por ser muy ilustrativo de la naturaleza del problema.

[2]  Recordar que en estos casos las funciones-operador son buenas candidatas para ser definidas inline ( 4.11.2a).

[3]  Stroustrup TC++PL  §5.5 References: "Las referencias también pueden utilizarse para definir funciones que pueden ser utilizadas en ambos lados, izquierdo y derecho, de una asignación".