¡Nuevo!  por fin disponible la versión 5 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.18a   Sobrecarga del operador de asignación

§1  Preámbulo

Lo mismo que ocurre con el constructor-copia ( 4.11.2d4), si en la definición de una clase no se sobrecarga explícitamente el operador de asignación = ( 4.9.2), el compilador proporciona una versión por defecto. Esta versión "de oficio" realiza una asignación miembro a miembro de los elementos de la clase. De esta forma, aunque no se haya definido explícitamente la función operator= en la definición de UnaClase, son posibles expresiones como:

class UnaClase { ... };
...
UnaClase c1;
c1 = c2;


Ejemplo

Veámoslo con un ejemplo en el que definimos la clase Vector, destinada a representar los vectores libres en un espacio de dos dimensiones. La clase incluye dos propiedades, x e y, que son las componentes escalares del vector respecto a un sistema ejes cartesianos.

#include <iostream>
using namespace std;
class Vector {                // definición de la clase Vector
  public: float x, y;
};

void main () {                // ==================
  Vector v1, v2;              // M.1:
  v1.x = 1.0;  v1.y = 2.0;    // M.2:
  Vector v2 = v1;             // M.3: Uso sobrecargado del operador =
  cout << "x1 = " << v1.x << " y1 = " << v1.y << endl;
  cout << "x2 = " << v2.x << " y2 = " << v2.y << endl;
}

Salida:

x1 = 1 y1 = 2
x2 = 1 y2 = 2

Comentario

En ausencia de ninguna definición explícita, el compilador proporciona "de oficio" un constructor ( 4.11.2d1); un constructor-copia ( 4.11.2d4), y una versión sobrecargada del operador de asignación =  adecuados a la definición de la clase.

En M.1 se instancian sendos objetos v1 y v2, sus valores son iniciados por el constructor de oficio. En M.2 los miembros de v1 son modificados a valores determinados, y en M.3 se utiliza la versión sobrecargada "de oficio" del operador = para asignar el objeto v1 a v2.

Las salidas muestran como efectivamente, la versión oficial del operador ha realizado una asignación miembro a miembro del operando v1 sobre v2.

Observe que en M.2 se utiliza una versión del operador = para la clase de los float preconstruida de forma "nativa" en el lenguaje. Esta versión no puede ser sobrecargada y la denominamos global. En otras palabras: el comportamiento del operador = para objetos tipo float está predeterminado en el lenguaje y su comportamiento no puede ser modificado.  La versión global de un operador no puede ser sobrecargada.

§2  Sinopsis

Existen ocasiones en que la versión "de oficio" del operador de asignación = no es adecuada, por lo que el lenguaje ofrece la posibilidad de sobrecargarlo para que se adapte a un comportamiento específico. Como se ha indicado ( 4.9.18), la versión explícita del operador de asignación se establece declarando una función miembro no estática operator=.  Ejemplo:

Vector operator= (Vector v) { ... } ;     // función-operador

Cuando se sobrecarga explícitamente el operador de asignación, el compilador establece la limitación de que esta versión sobrecargada no es heredada por las posibles clases derivadas ( 4.11.2b).

§3  Ejemplo

Veamos un ejemplo más concreto en el que a la clase Vector del ejemplo anterior le sobrecargamos el operador de asignación simple =, de forma que la asignación entre vectores realice al mismo tiempo una multiplicación por un escalar [1]:

#include <iostream>
using namespace std;
class Vector {                         // definición de la clase Vector
  public:
  float x, y;
  void  operator= (Vector v) {        // L.6: función-operador
    x = v.x * 10;                      // L.7:
    y = v.y * 10;                      // L.8:
  }
};

void main () {                // ==================
  Vector v1, v2, v3;
  v1.x = 1.0;  v1.y = 2.0;
  v2 = v1;                    // M.3:
  v3 = v2;                    // M.4:
  cout << "x1 = " << v1.x << " y1 = " << v1.y << endl;
  cout << "x2 = " << v2.x << " y2 = " << v2.y << endl;
  cout << "x3 = " << v3.x << " y3 = " << v3.y << endl;
}

Salida:

x1 = 1 y1 = 2
x2 = 10 y2 = 20
x3 = 100 y3 = 200

Comentario

El programa compila sin dificultad y las salidas son las esperadas; confirmando que las asignaciónes M.3 y M.4 invocan la versión sobrecargada de operator= y que esta funciona correctamente.

Sin embargo, a pesar de su aparente idoneidad, si en el ejemplo anterior sustituimos las sentencias M.3, M.4 por una asignación compuesta:

v3 = v2 = v1;       // M.3bis:

  la nueva sentencia produce un error de compilación [2]. La razón es que nuestra versión sobrecargada de operator= viola una de las reglas básicas de los operadores C++ de asignación: producir un resultado adecuado además de realizar la asignación en sí misma ( 4.9.2). Así pues, la ejecución de M.3bis, que se realiza de derecha a izquierda, sería adecuada en su primera parte:  v2 = v1, pero el resultado, void (L.6) debe ser aplicado a la siguiente: v3 = void, y el compilador no encuentra una función-operador adecuada en la que esté definida una asignación del tipo:

<valor-devuelto>  operator= (void);   // declaración esperada

Comprobamos que, en este sentido, el mensaje de error del compilador Borland es quizás el más explícito.

§4  Para conseguir un funcionamiento correcto, modificamos la definición de operator=:

#include <iostream>
using namespace std;
class Vector {              // definición de la clase Vector
  public:
  float x, y;
  Vector operator = (Vector v) { // L.6: función-operador
    x = v.x * 10;
    y = v.y * 10;
    return *this;           // L.10:
  }
};

void main () {              // ==================
  Vector v1, v2, v3;        // M.1:
  v1.x = 1.0;  v1.y = 2.0;  // M.2:
  v3 = v2 = v1;             // M.3:  Ok.
  cout << "x1 = " << v1.x << " y1 = " << v1.y << endl;
  cout << "x2 = " << v2.x << " y2 = " << v2.y << endl;
  cout << "x3 = " << v3.x << " y3 = " << v3.y << endl;
}

Salida:

x1 = 1 y1 = 2
x2 = 10 y2 = 20
x3 = 100 y3 = 200

Comentario

Observe en L.10, la utilización explícita del puntero this  ( 4.11.6) en la expresión del valor devuelto por la función operator=; como la aplicación del operador de indirección * ( 4.9.11a) sobre dicho puntero, devuelve un objeto, y como este objeto es precisamente el primer operador (Lvalue) involucrado en la expresión  a = b;. Recuerde que a = b; es equivalente a: a.operator=(b);.

Nota: se verá en el siguiente epígrafe que la invocación de una función como operator+() del ejemplo, implica dos invocaciones al constructor-copia ( 4.11.2d4), por lo que es muy posible que algunos diseños requieran definir explícitamente dicho constructor si se sobrecarga el operador de asignación.

§5  Una versión definitiva:

A pesar de que según hemos comprobado, la versión anterior funciona correctamente, en la práctica la versión sobrecargada del operador de asignación suele adoptar la siguiente forma genérica:

ClaseX& operator= (const ClaseX& obj) {
   ...                        // asignaciones
   return valordevuelto;     // generalmente *this
}

Este diseño hace que la función reciba y devuelva sendas referencias en vez de objetos, lo que disminuye la sobrecarga inherente a la creación de objetos temporales por el compilador. A su vez, el argumento de la función se declara const ( 3.2.1c) para asegurar que operator= no modificará el objeto utilizado como argumento (lo que significaría modificar el Rvalue de la asignación).

En nuestro caso, una expresión del tipo v1 = v2 equivale a la invocación:

v1.operator=(const Vector& v2);


Para justificar y poner en evidencia la economía de proceso derivada de utilizar referencias, efectuaremos un experimento. Para ello añadimos a la versión anterior un constructor-copia ( 4.11.2d4) y un constructor por defecto ( 4.11.2d1) explícitos [3],  haciendo que nos muestren los objetos creados.

#include <iostream>
using namespace std;

class Vector {                   // definición de Vector
  public:
  float x, y;
  Vector operator = (Vector v) { // L.6: función-operador
    x = v.x * 10;
    y = v.y * 10;
    return *this;                // L.9
  }
  Vector(int i = 0, int j = 0) {
    cout << "Creado un objeto (1)" << endl;
    x = i; y = j;
  }
  Vector(Vector& v) {            // constructor-copia
    cout << "Creado un objeto (2)" << endl;
    x = v.x; y = v.y;
  }
};

void main () {                   // ==================
  Vector v1, v2, v3;             // M.1:
  v1.x = 1.0; v1.y = 2.0;
  v3 = v2 = v1;                  // M.3:
  cout << "x1 = " << v1.x << " y1 = " << v1.y << endl;
  cout << "x2 = " << v2.x << " y2 = " << v2.y << endl;
  cout << "x3 = " << v3.x << " y3 = " << v3.y << endl;
}

Salida [4]:

Creado un objeto (1)
Creado un objeto (1)
Creado un objeto (1)
Creado un objeto (2)
Creado un objeto (2)
Creado un objeto (2)
Creado un objeto (2)
x1 = 1 y1 = 2
x2 = 10 y2 = 20
x3 = 100 y3 = 200

Comentario

Las tres primeras salidas corresponden a la invocación implícita al constructor por defecto realizadas en M.1 para instanciar los objetos v1, v2 y v3. Las siguientes corresponden a sendas invocaciones al constructor-copia en M.3. Recuerde que esta última sentencia equivale a:

v2 = v1;
v3 = v2;

y que en realidad, cada una de ellas adopta la forma  vL.operator=(vR) (al Lvalue y Rvalue de la asignación los hemos designado respectivamente vL y vR ). La invocación de operator=( ) supone la creación de un objeto v que es el argumento de la función y es local a esta (se destruye cuando la función es descargada de la pila ( 4.4.6b). Esta es la primera invocación al constructor-copia.  La sentencia de retorno (L.9) implica la creación de otro objeto temporal *this, que será devuelto (por valor ). Esta es la segunda invocación al constructor-copia.

Este último extremo puede comprobarse modificando el diseño de operator=():

void operator = (Vector v) {       // L.6b: función-operador bis
   x = v.x * 10;
   y = v.y * 10;
// return *this;
}

Con esta definición solo se realiza la primera invocación al constructor-copia (aunque por supuesto el operador no pueda ser utilizado para asignaciones en cadena ).


Si en el diseño anterior cambiamos la definición de la función-operador por una que utilice referencias:

  Vector& operator = (const Vector& v) {   // L.6c: función-operador
     x = v.x * 10;
     y = v.y * 10;
     return *this;
  }

Se obtienen las siguientes salidas:

Creado un objeto (1)
Creado un objeto (1)
Creado un objeto (1)
x1 = 1 y1 = 2
x2 = 10 y2 = 20
x3 = 100 y3 = 200

Como puede suponer, esta versión resulta mucho más eficiente que la anterior, ya que la creación y destrucción de cuatro objetos requiere tiempo y memoria, en especial cuando se trata de objetos grandes.

Otro ejemplo de sobrecarga del operador de asignación ( 4.9.18d1).


§6  Como resumen de lo anteriormente expuesto, incluimos un diseño de lo que sería una versión real para la clase Vector con su correspondiente operador de asignación sobrecargado.

class Vector {      // Una clase Vector
  public:
  float x, y;
  Vector& operator= (const Vector& v) { // operador de asignación
    x = v.x;  y = v.y;
    return *this;
  }
  Vector(int i = 0, int j = 0) {        // constructor por defecto
    x = i; y = j;
  }
  Vector(const Vector& v) {             // constructor-copia
    x = v.x; y = v.y;
  }
};

  Inicio.


[1]  Que una definición de este tipo tenga o no sentido desde el punto de vista matemático es otra cuestión ( 4.9.18). En este aspecto, el lenguaje C++ es extraordinariamente permisivo y en cualquier caso, el ejemplo propuesto es solo una muestra de su posibilidades.

[2]  El mensaje es más o menos explícito, dependiendo del compilador utilizado. Por ejemplo, MS Visual C++ 6.0: binary '=' : no operator defined which takes a right-hand operand of type 'void' (or there is no acceptable conversion);  Borland C++ 5.5: Could not find a match for 'Vector::operator =(void)' in function main(), y con Linux GCC v 2.95.3 19991030 (prerelease): No match for 'Vector & = void', indicándonos además que miremos la definición de L.6.

[3]  En realidad solo necesitamos el constructor-copia, pero su inclusión nos obliga a definir también explícitamente un constructor por defecto.

[4]  La salida indicada corresponde al compilador MS Visual C++ 6.0.  Por su parte, Borland C++ 5.5 realiza algún tipo de optimización interna y solo realiza tres invocaciones al constructor-copia en la sentencia M.3. Aunque sigue realizando dos para una asignación simple del tipo v2 = v1;.