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.18b1  Sobrecarga de operadores relacionales

§1  Sinopsis

Al tratar de los operadores relacionales ( 4.9.12) se indicó que los operandos deben ser tipos aritméticos o punteros. Cuando estas condiciones no se cumplen. Por ejemplo, con tipos definidos por el usuario, estos operadores pueden y deben ser sobrecargados. En especial si necesitamos utilizar con ellos expresiones relacionales.

Como el resto de los operadores binarios, si @ representa un operador relacional, la expresión x @ y puede ser interpretada de dos formas [2]:

a.  Mediante una función miembro no estática que acepte un argumento: x.operator@(y)

b.  Mediante una función no miembro (generalmente friend) que acepte dos argumentos: operator@(x, y)

§2  Permanencia de las leyes formales

Aunque el programador tiene cierta libertad en el modo de sobrecargar los operadores [1], conviene mantener una cierta coherencia en su comportamiento, de forma que sus propiedades formales se mantengan también en la versión sobrecargada ( 4.9.18). En el caso de los operadores relacionales estas propiedades podrían ser sintetizarlas en los puntos siguientes [3]:

Nota: para los ejemplos que siguen nos referimos a objetos de la clase Vector ya utilizada en el capítulo anterior. Tiene solo dos propiedades, que corresponden con las componentes escalares de un vector en el plano:

class Vector { public:  float x, y; }

§2.1  Que ambos operandos sean del mismo tipo

Esta condición exige que ambos operandos sean instancias de la misma clase, o al menos de la misma jerarquía.  Por ejemplo, una definición del operador de identidad == del tipo:

bool operator== (Vector v, int i) {
  return ((v.x == i) && (v.y == i))? true: false;
}

que estableciera el resultado de comparar un objeto tipo Vector con otro tipo int, probablemente no tendría mucho sentido. Además, la posibilidad de establecer la identidad de un vector "por la derecha" con un tipo intv1 == 10, nos obligaría a establecer también la identidad "por la izquierda": 10 == v1, con otra definición del tipo:

bool operator== (int i, Vector v) {
  return ((v.x == i) && (v.y == i))? true: false;
}

lo que a su vez nos obligaría a sobrecargarlo para tipos Vector, ya que si:  v1 == 10 && v2 == 10, debería cumplirse también que v1 == v2.

De acuerdo con estas consideraciones, parece más razonable una definición sobrecargada para el operador identidad del tipo:

bool operator== (Vector& v1, Vector& v2) {
   return ((v1.x == v2.x) && (v1.y == v2.y))? true: false;
}

Observe que se han utilizado argumentos pasador "por referencia" para disminuir la sobrecarga inherente a la copia de objetos ( 4.2.3).

§2.2  Que devuelvan un bool

Por supuesto, nada nos impide definiciones del tipo:

float operator== (Vector v1, Vector v2) {
  return ((v1.x == v2.x) && (v1.y == v2.y))? 1.0 : 2.0;
}

En este caso, el resultado de aplicar el operador de identidad entre dos objetos no devuelve un valor lógico (cierto o falso), sino un entero. El inconveniente es que estamos sentando las bases de una "lógica" bastante desconcertante para nuestro programa. Considere la salida obtenida en el siguiente ejemplo:

#include <iostream>
using namespace std;

class Vector {
  public: int x, y;
  float operator== (Vector v) {
    return ((v.x == x) && (v.y == y))? 1.0: 2.0;
  }
};

void main() {    // ================
  Vector v1 = {2, 3}, v2 = {4, 5};
  if ( v1 == v2 )
    cout << "Iguales" << endl;
  else
    cout << "Distintos" << endl;
}

Salida:

Iguales

Observe que en este ejemplo se ha utilizado la primera opción sintáctica , "Declarar una función miembro no estática que acepte un argumento" para la definición de la función operator==.

§2.3  Que se cumplan las propiedades conmutativa, reflexiva, simétrica y transitiva.

Esto significa que si, por ejemplo, definimos el operador mayor que > de forma que la cláusula ( a > b ) resulte cierta, entonces debería definirse el operador < de forma que la cláusula ( b < a ) resultara igualmente cierta.

Considere el resultado obtenido en el siguiente ejemplo en el que sobrecargamos los operadores > y < de forma bastante desafortunada:

#include <iostream>
using namespace std;

class Vector {
  public: int x, y;
  bool operator== (Vector v) {   // sobrecarga operador ==
    return ((v.x == x) && (v.y == y))? true: false;
  }
  bool operator> (Vector v) {    // sobrecarga operador >
    return ((x > v.x) || (y > v.y))? true: false;
  }
  bool operator< (Vector v) {    // sobrecarga operador <
    return ((x < v.x) || (y < v.y))? true: false;
  }
};

void main() {    // ===============
  Vector v1 = {2, 1}, v2 = {3, 0};

  if ( v1 == v2 ) cout << "Iguales" << endl;
  else            cout << "Distintos" << endl;
  if ( v1 > v2 )  cout << "v1 mayor que v2" << endl;
  if ( v1 < v2 )  cout << "v1 menor que v2" << endl;
}

Salida:

Distintos
v1 mayor que v2
v1 menor que v2


Siguiendo con este orden de ideas, recordar por ejemplo que el operador de identidad == debería ser sobrecargado de forma que esta relación binaria entre los objetos de la clase, fuese reflexiva; simétrica, y transitiva:

a == a                                        Propiedad  Reflexiva

a == b     b == a                     Propiedad Simétrica

a == b  y  b == c      a == c     Propiedad Transitiva

Recordar que la desigualdad != debe ser simétrica:

a != b    b != a

§3  Ejemplo:

Como resumen de los conceptos anteriores, se expone una posible solución a la sobrecarga de estos operadores para la clase Vector.

#include <iostream>
using namespace std;

class Vector {
  public: int x, y;
  bool operator== (Vector& v) {
    return ((v.x == x) && (v.y == y))? true: false;
  }
  bool operator!= (Vector& v) {
    return !(*this == v);
  }
  bool operator> (Vector& v) {
    return ((x * x + y * y) > (v.x * v.x + v.y * v.y))? true: false;
  }
  bool operator< (Vector& v) {
    return ((x * x + y * y) < (v.x * v.x + v.y * v.y))? true: false;
  }
  bool operator>= (Vector& v) {
    return ((x * x + y * y) >= (v.x * v.x + v.y * v.y))? true: false;
  }
  bool operator<= (Vector& v) {
    return ((x * x + y * y) <= (v.x * v.x + v.y * v.y))? true: false;
  }
};

void main() {   // ===============
  Vector v1 = {2, 1}, v2 = {3, 0};
  if ( v1 == v2 ) cout << "Iguales" << endl;
  if ( v1 != v2 ) cout << "Distintos" << endl;
  if ( v1 > v2 ) cout << "v1 mayor que v2" << endl;
  if ( v1 < v2 ) cout << "v1 menor que v2" << endl;
  if ( v1 >= v2 ) cout << "v1 mayor o igual que v2" << endl;
  if ( v1 <= v2 ) cout << "v1 menor o igual que v2" << endl;
}

Salida:

Distintos
v1 menor que v2
v1 menor o igual que v2

Comentario

Si (como suponemos) se trata de vectores de un espacio Euclídeo de dos dimensiones, la definición establecida para el operador de identidad, define una relación de equipolencia o igualdad geométrica de vectores, que garantiza la permanencia de las propiedades reflexiva, simétrica y transitiva para la relación.

Observe que el operador de desigualdad != se ha definido como negación del resultado de la identidad (ambos operadores definen propiedades excluyentes). Observe también la utilización explícita del puntero this ( 4.11.6)

El resto de operadores se ha definido en función de magnitudes escalares (los módulos de los vectores). En realidad empleamos un grupo de transformaciones que utilizan indirectamente las versiones globales de los operadores, con lo que tenemos garantizado que los resultados y el álgebra empleada son coherentes.


§4  En la página adjunta se muestra un diseño de la clase Vector que habíamos visto en el capítulo anterior ( 4.9.18a). Aquel diseño básico lo completamos con los operadores relacionales aprendidos en esta lección ( Ejemplo).

  Inicio.


[1]  La libertad es relativa, ya que no puede cambiar el número de operandos o la asociatividad y precedencia del operador.

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

[3]  Puesto que, a la postre, es el propio programador el responsable de su utilización, dentro de las amplias posibilidades ofrecidas por el lenguaje, es perfectamente factible sobrecargar los operadores para crear un álgebra más o menos enloquecida y una lógica más o menos racional. Por ejemplo, objetos donde a == b no implique necesariamente que b == a, o que después de a = b no se cumpla que a == b. Que tales situaciones sean útiles o no, dependen naturalmente de las circunstancias concretas. Así pues, los consejos que siguen son meramente indicativos de la operatoria "normal" que conducirá a resultados de lógica más cotidiana y a un álgebra más tradicional.