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.18g  Sobrecarga de operadores lógicos

§1  Sinopsis:

Recordemos ( 4.9.8) que los operadores lógicos suelen ser representados por sus nombres en inglés (mayúsculas):  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 negación NOT, acepta solo uno. Recordemos también que los operandos de las versiones globales de estos operadores son convertidos a un tipo bool, y que el resultado es también un tipo bool de valor cierto/falso (true/false).

Cualquier intento de aplicar estos operadores a tipos abstractos ( 2.2) genera un error de compilación recordándonos que la operación solo está definida para los tipos básicos (preconstruidos en el lenguaje). La solución en estos casos es sobrecargar adecuadamente estos operadores para los miembros de la clase, lo que puede realizarse mediante los procedimientos estándar ya señalados 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 señalado 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 básicos. 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 lógico sería que fuese un bool, con objeto que el resultado de estas operaciones fuese el que se espera intuitivamente. Además de esto, considere que los tres operadores están relacionados desde el punto de vista lógico, y deberían seguir estándolo de la misma forma en las versiones sobrecargadas.

Por ejemplo, si definimos la sobrecarga del operador de negación lógica NOT de forma que para los objetos c1 y c2 resulta:

!c1 == false
!c2 == false

Deberíamos definir la sobrecarga del operador AND de forma que

(c1 && c2) == true

De lo contrario estaríamos construyendo para los objetos de tipo C una lógica bastante difícil de comprender a una mente acostumbrada al razonamiento estándar con los tipos básicos del lenguaje.

En este capítulo trataremos de demostrar que si se quiere mantener una lógica coherente, la sobrecarga de los tres operadores lógicos AND, OR y NOT para tipos de una clase C, puede ser sustituida por una conversión de usuario mediante una función de conversión operator bool()  adecuada ( 4.9.18k).

§3  Sobrecarga del operador NOT

El operador NOT de negación lógica ( ! ) está relacionado con su contrario (que no tiene nombre ni representación). Para explicar el significado de esta afirmación, supongamos un objeto c de una clase C. Según hemos señalado, un intento de utilizarlo, por ejemplo, la expresión:

if (!c) { /* ... */ }

genera un error de compilación: 'operator!' not implemented in type 'C'..., en el que se indica que el operador NOT de negación lógica no está definido para objetos de la clase. Sin embargo, podemos observar que un intento análogo sin el operador:

if (c) { /* ... */ }

también produce un error, aunque en este caso la indicación es más ambigüa: 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 expresión entre paréntesis (c) a un tipo bool. Recuerde que la sentencia if(<condicion>)... espera recibir una expresión <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 conversión 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 lógicas con estos objetos, consideramos "ciertos" los puntos que pertenecen al primer cuadrante y "falsos" todos los demás. Un posible diseño sería 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() {    // conversión 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 definición de operator bool debe ser congruente con la definición de operator!, de forma que un objeto no pueda ser cierto y falso al mismo tiempo.

§3.2  Observe también que una vez definida la función de conversión operator bool, la de negación no es realmente necesaria. En efecto: la definición

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() {    // conversión de usuario
    return ((x > 0 && y > 0) ? true : false );
  }
};

produce exactamente las mismas salidas que la anterior. La razón es que en las expresiones (! p) el objeto p es convertido a tipo bool por acción de la versión global del propio operador NOT. Esta conversión se realiza mediante una invocación del tipo p.operator bool().


§3.3  Un último truco podría permitirnos la operatoria inversa: obtener los valores (p) mediante una doble negación (!!p). En cuyo caso podríamos eliminar la conversión de usuario operator bool, dejando la sobrecarga del operador de negación 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 conversión de usuario que garantice la conversión 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 definición (§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 razón es la señalada en el caso del operador NOT (§3.2 ). Si existe posibilidad de conversión de los operandos a tipos bool, entonces el compilador utiliza las versiones globales de ambos operadores una vez realizada la conversión 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 función miembro no estática que acepte un argumento

b.  Declarando una función 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 mantendrán coherencia con los principios utilizados al sobrecargar el operador NOT (§3.1 ). Recordemos que para las operaciones lógicas con estos objetos, consideramos "ciertos" los puntos que pertenecen al primer cuadrante y "falsos" todos los demás.

La sobrecarga de AND se realiza mediante el procedimiento a (una función miembro no estática); para OR se utiliza el procedimiento b (una función externa que acepte dos argumentos). El diseño es el siguiente:

#include <iostream>
using namespace std;

class V2D {
  public:
  float x, y;
  V2D(float i=0, float j=0): x(i), y(j) { } // constructor
  bool operator&&(V2D& p) {                 // función-operador
    if (x > 0 && y > 0) {
      if (p.x > 0 && p.y > 0) return true;
    }
    return false;
  }
  friend bool operator||(V2D&, V2D&);
};

bool operator||(V2D& p1, V2D& p2) {         // función-operador
  if (p1.x > 0 && p1.y > 0) return true;
  if (p2.x > 0 && p2.y > 0) return true;
  return false;
}

int main() {     // =================
  V2D p1(0,2);
  V2D p2(-1.1, 2);
  V2D p3 = V2D(1,2);
  V2D p4(1, -3);

  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 versión global del operador AND (&&). Observe también que en ambas definiciones se ha respetado la mecánica descrita en el Estándar 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 definición de operator||, si se cumple la primera comprobación (p1.x > 0 && p1.y > 0), se devuelve directamente un resultado true, sin necesidad de esperar a realizar la segunda.