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]


Operadores en jerarquías de clases

§1 Sinopsis

A excepción del operador de asignación simple ( = ), las funciones-operador operator@ de una súper-clase son heredadas en las clases derivadas; además, si B es base de la clase D, un operador @ sobrecargado para B puede ser sobrecargado más tarde para D.

Puesto que las funciones-operador se comportan en las jerarquías de clases como el resto de las funciones miembro, es posible la aplicación de operadores cuando los operandos son objetos de la misma familia. Por ejemplo b @ d, aunque este tipo de operaciones no está exenta de problemas. Para ilustrar el tipo de inconvenientes que pueden presentarse, analizamos un ejemplo en el que empleamos la suma binaria + entre objetos de una jerarquía.

§2 Ejemplo

Definimos la clase VectorL que representa vectores libres de un espacio de dos dimensiones; para su representación solo necesitamos dos coordenadas cartesianas. De esta clase derivamos VectorF para representar los vectores fijos en el mismo plano. A la nueva clase le añadimos dos nuevos miembros que representen las coordenadas del punto de aplicación del vector.

#include <iostream>
using namespace std;

class VectorL {                   // Vector libre
  public: float x, y;
  VectorL operator+ (VectorL v) {
    VectorL vr;
    vr.x = x + v.x;
    vr.y = y + v.y;
    return vr;
  }
};

class VectorF : public VectorL {  // Vector fijo
  public: float px, py;
};

void main() {            // =============
  VectorL vl1 = {1, 2}, vl2 = {3, 5};
  VectorF vf1, vf2, vf3;
  vf1.x = 3; vf1.y = 5; vf1.px = 6; vf1.py = 7;
  vf2.x = 4; vf2.y = 6; vf2.py = 8; vf2.py = 9;
  vf1 = vl1 + vf2;            // M.5 Error !!
  vl2 = vl1 + vf1;
  cout << "x = " << vl2.x << " y = " << vl2.y << endl;
  vl2 = vf1 + vl1;            // M.8
  cout << "x = " << vl2.x << " y = " << vl2.y << endl;
  vf3 = vf1 + vf2;            // M.10: Error !!
}

Salida (una vez eliminadas las sentencias erróneas):

x = 4 y = 7
x = 4 y = 7

Comentario

El operador suma + se ha definido como función-miembro de la superclase VectorL. Aunque en la clase derivada VectorF existe una versión heredada, es análoga a la original; espera un argumento de tipo VectorL y devuelve un vector del mismo tipo.

Como puede verse, el compilador nos avisa que la operación M.5 no es posible: Cannot convert 'VectorL' to 'VectorF'. La explicación está en que el resultado de la suma, un objeto tipo VectorL, debe ser aplicado a un objeto VectorF, cosa que no es posible porque no se ha definido el operador de asignación = entre ambos tipos.

Nota: podría pensarse que el error está en la expresión suma contenida en el Rvalue, pero en realidad no es así. La suma vl1 + vf2 entre objetos VectorL y VectorF se ejecuta sin problemas; puede comprobarse sustituyendo la sentencia M.5 por

vl1 + vf2;    // M.5bis Ok.!!

La razón ahora es que la invocación vl1.operator+(vf2) en que se transforma esta sentencia, es perfectamente válida; el compilador puede realizar una promoción automática del argumento actual al formal.

Las sentencias M.6 y M.8 se ejecutan sin problemas; los resultados son los esperados, y comprobamos que la suma resulta conmutativa.

En M.10 obtenemos un nuevo error: Cannot convert 'VectorL' to 'VectorF'. En realidad se trata del mismo problema que el caso anterior. Aunque ambos operandos en la suma son tipo VectorF, no olvidemos que la función operator+ se ha definido para la clase VectorL; espera y devuelve objetos de este tipo, por lo que el compilador realiza las promociones adecuadas y ejecuta la suma sin dificultad (puede comprobarse cor el mismo método que en M.5).

Lo que no es posible es aplicar el Rvalue resultante (un tipo VectorL a un Lvalue que es tipo VectorF).

§3 Una solución que causa nuevos problemas:

Una posible solución para resolver este último problema, es definir también la función operator+ en la clase VectorF; de este modo el Rvalue de la sentencia M.10 será un tipo VectorF y la asignación será posible. El nuevo diseño de la clase queda como sigue:

class VectorF : public VectorL {     // Vector fijo
  public: float px, py;
  VectorF operator+ (VectorF v) {    // versión sobrecargada para tipos VectorF
    VectorF vr;
    vr.x = x + v.x; vr.y = y + v.y;
    vr.px = px + v.px; vr.py = py + v.py;
    return vr;
  }
};

Una vez realizadas las modificaciones oportunas, el programa compila y ejecuta sin dificultad la sentencia M.10, pero surge un nuevo problema. La sentencia M.8, que compilaba correctamente en la versión inicial, presenta ahora un error: 'operator+' not implemented in type 'VectorF' for arguments of type 'VectorL'.


Con el diseño inicial, la expresión M.8 vl2 = vf1 + vl1; se ejecutaba de izquierda a derecha como sigue:

  1. Los objetos vf1 y vl1 son sumados mediante una adecuada promoción de vf1 al tipo adecuado (VectorL), ya que operator+ se ha definido para dicha clase.
  2. El resultado, un objeto tipo VectorL, es aplicado a vl2 sin problemas.

Sin embargo, en el nuevo diseño ya existe una función-operador + para el tipo VectorF; el compilador intenta utilizarla pero no existe concordancia completa en los argumentos (ni posibilidad de promoción), lo que provoca el error señalado.

  Es súmamente instructivo comprobar en este ejemplo el resultado de las reglas de congruencia estándar de argumentos ( 4.4.1a). En la clase derivada VectorF existen en realidad dos funciones-operador; una es privativa, la otra heredada ( 4.11.2b). Estas funciones son equivalente a:

operator+(VectorL, VectorL);     // Heredada de VectorL §a
operator+(VectorF, VectorF);     // Privativa           §b


Sabemos que la expresión vf1 + vl1 es transformada por el compilador en una llamada del tipo: operator+(vf1, vl1), y que siguiendo sus reglas, intenta utilizar la versión §b, que proporciona concordancia con el primer argumento, pero encuentra que no existe concordancia en el segundo (tipo VectorL). Es justamente el mensaje de error que nos devuelve.

Observe que la única posibilidad, la promoción del objeto VectorL a tipo VectorF, no es posible; este último tiene miembros que no existen en el primero. Sin embargo, la inversa si es factible; la promoción de un objeto VectorF a VectorL simplemente exige ignorar algunos miembros.

Resulta igualmente ilustrativo comprobar que (como sospechamos), si cambiamos el orden de los sumandos en M.8 el programa compila sin dificultad:

vl2 = vl1 + vf1;      // M.8bis Ok.!!


En efecto, esta la sentencia presenta un panorama distinto: la función operator+(vl1, vf1) presenta congruencia en el primer argumento con la versión heredada §a. Aunque el segundo argumento (tipo VectorF) debe ser promovido a VectorL, hemos visto que puede realizarse sin dificultad. El resultado es del mismo tipo que el objeto (vl2) al que será aplicado en la asignación subsiguiente.

Observe que las asignaciones M.6, M.8 y M.10 se realizan con la versión "de oficio" del operador = proporcionada por el compilador ( 4.9.18a).

§4 Más sorpresas

En el ejemplo original, la versión sobrecargada de operator+ en VectorL, se realizó en lo que hemos denominado la versión a ( 4.9.18b), es decir: "declarando una función miembro no estática que acepte un argumento". A continuación repetimos el ejemplo, pero cambiando la definición por la versión b: "una función no miembro (generalmente friend) que acepte dos argumentos".

Después de eliminar las líneas que causaban problema y efectuar el cambio, el programa y los resultados obtenidos son los siguientes:

#include <iostream>
using namespace std;

class VectorL {             // Vector libre
  public: float x, y;
  friend VectorL operator+ (VectorL, VectorL);
};

VectorL operator+ (VectorL v1, VectorL v2) { // Función-operador externa
  VectorL vr;
  vr.x = v1.x + v2.x;  vr.y = v1.x + v2.y;
  return vr;
}

class VectorF : public VectorL {  // Vector fijo
  public: float px, py;
};

void main() {              // =====================
  VectorL vl1 = {1, 2}, vl2 = {3, 5};
  VectorF vf1, vf2, vf3;
  vf1.x = 3; vf1.y = 5; vf1.px = 6; vf1.py = 7;
  vf2.x = 4; vf2.y = 6; vf2.py = 8; vf2.py = 9;

  vl2 = vl1 + vf1;
  cout << "x = " << vl2.x << " y = " << vl2.y << endl;
  vl2 = vf1 + vl1;         // M.8
  cout << "x = " << vl2.x << " y = " << vl2.y << endl;
}

Salida:

x = 4 y = 6
x = 4 y = 5

Comentario:

Supuestamente, el cambio de la definición a a la b no debería afectar a la definición del operador +, dado que ambas son equivalente, sin embargo las salidas no coinciden!! con las obtenidas al principio .

A la luz de las consideraciones expuestas, intente el lector encontrar una explicación por si mismo a estos sorpresivos resultados. Después de establecida una primera hipótesis, intente imaginar y realizar algún pequeño experimento para verificar que sus suposiciones son correctas. Realice igualmente cuantas pruebas (o pequeños programas auxiliares) estime necesarios para esclarecer aquellos puntos en los que tenga duda.

Una pista: Repase las propiedades del especificador friend ( 4.11.2a1).

§5 Corolario

Como habrá podido comprobar a lo largo del ejemplo y del resto de este "libro", el lenguaje C++ es extraordinariamente potente y rico en detalles, pero también extraordinariamente sutil. Es una gran autopista, pero llena de trampas que acechan en el arcén. Justamente esta complejidad de sus detalles hace casi imposible programar con él nada medianamente serio "sin haber asimilado antes, hasta la médula de los huesos, los principios que rigen su comportamiento" [1] y relativamente fácil volarse una pierna, o terminar limpiando cochineras en Carolina del Norte ( 1).

  Inicio.


[1] Tomo prestada la frase de un ilustre ingeniero Español, Eduardo Torroja. La pronunció en otro contexto, pero viene muy al caso.