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.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;.