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.18e  Sobrecarga del operador ->

§1  Antecedentes

El selector indirecto de miembro -> ( 4.9.16) es un operador binario [1] que permite acceder a miembros de objetos cuando se dispone de punteros a la clase correspondiente. Una expresión del tipo Cpt->membr representa el miembro de identificador membr de la clase Cl siempre que Cpt sea un puntero a dicha clase. Ejemplo:

class Cl {
  public: int x;
} c1, *ClPt = &c1;
...
ClPt->x = 10;


Sabemos que la expresión ClPt->x  exige que el primer operando Clpt sea un puntero a la clase, y que el segundo x, sea el identificador de uno de sus miembros.

§2  Sinopsis

La gramática C++ permite definir una función miembro operator-> que puede ser invocada con la sintaxis del operador selector indirecto de miembro ->.  Por ejemplo, siendo obj una instancia de la clase Cl para la que se define la función operator->, y membr un miembro de la misma [2], la expresión:

obj->membr;

es transformada por el compilador en una invocación del tipo:

( obj.operator->() )->membr;


La parte entre paréntesis obj.operator->(), representa la invocación del método operator-> sobre el objeto obj. Puesto que el valor devuelto por la función será considerado a su vez el primer operando de -> aplicado a membr, la función operator-> debe devolver un puntero a un objeto de la clase sobre el que se pueda aplicar el operador ->. Es decir, su diseño debe tener el siguiente aspecto:

class Vector {
  ...
  Vector* operator-> () {
    ...
    return this;
  }
};

Observe que el puntero que se obtiene como resultado de la invocación obj.operator->() no depende de la naturaleza del operando membr. Por esta razón se considera a veces que operator-> es un operador unario posfijo ( 4.9). Lo que significa que una expresión como

v.operator->();

tiene sentido y devuelve un puntero al objeto:

Vector v1;

Vector* vptr;

vptr = v1.operator->();  // Ok! vptr señala ahora a v1

§3  Condiciones

Para conseguir este comportamiento el compilador impone ciertas limitaciones, de forma que la función operator-> solo puede ser sobrecargada cumpliendo simultáneamente las siguientes condiciones:

a.  Ser una función-miembro no estática (que incluya el puntero this como argumento implícito 4.11.6).

b.  Ser una función-miembro que no acepte argumentos.

Ejemplo:

class Cl  {
  ...
  friend Cl* operator->();          // Error debe ser una función-miembro
  Cl* operator->(int i) {/*...*/}   // Error no acepta argumentos
  Cl* operator->() {/*...*/}        // Ok.
};

§4  El operador -> no puede ser sobrecargado

Aunque esta afirmación puede parecer escandalosa, ya que está en contradicción con el título del capítulo. Y además, en cualquier bibliografía que se consulte, la descripción de la función operator-> se encuentra siempre en el capítulo dedicado a la sobrecarga de operadores [3]. Sin embargo, no se trata de una verdadera sobrecarga del selector indirecto ->. Al menos, no en el sentido en que este mecanismo funciona con el resto de operadores.

Observe que en realidad, el compilador se limita a sustituir el primer operando de la expresión obj->membr por la invocación a una función-miembro, y una posterior utilización del resultado como primer operando de la versión global del operador, mientras el segundo operando se mantiene invariable. Además, en dicha expresión (invocación de la función-miembro), el primer operando debe ser necesariamente un objeto (instancia) de la clase y no un puntero Cl* como exige el uso regular del selector ->.


§4.1  Este comportamiento, distinto de aquellos casos en que la versión global del operador es sustituida "realmente" por la versión sobrecargada, puede verificarse con un sencillo ejemplo:

#include <iostream>
using namespace std;

class Vector {
  public:
  int x, y;
  bool operator== (Vector v) {  // L6: sobrecarga operador ==
    cout << "Invocada funcion operator==() " << endl;
    return ((v.x == x) && (v.y == y))? true: false;
  }
  Vector* operator-> () {       // L10: sobrecarga? operador ->
    cout << "Invocada funcion operator->() " << endl;
    return this;
  }
};

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

  cout << ( ( v1 == v2 )? "Iguales" : "Distintos" ) << endl;  // M.4
  cout << "v1.x == " << vptr->x << endl;                      // M.5
}

Salida:

Invocada funcion operator==()
Distintos
v1.x == 2

Comentario

Como puede verse, la utilización del operador de identidad == en M.4, provoca la utilización de la versión sobrecargada ( 4.9.18b1). Así mismo, la ausencia de la definición de este operador (L.6), habría producido un error de compilación al tratar de utilizarlo en M.4:

'operator==' not implemented in type 'Vector' for arguments of the same type in ...

Esto significa lisa y llanamente que el compilador no proporciona una versión por defecto de este operador (de identidad) para los objetos de la clase Vector.

En cambio, la utilización de la (supuesta) versión sobrecargada del selector indirecto de miembro -> (M.5), no produce la invocación automática de la misma como ocurrió en el caso de la identidad.  En realidad, ante expresiones del tipo vptr->x como en M.5, el compilador sigue utilizando la versión global (por defecto) del operador.


§4.2  Si en el ejemplo anterior sustituimos las líneas M.4/5 por:

cout << "v1.x == " << v1->x << endl;                   // §a
cout << "v1.x == " << v1.operator->()->x << endl;      // §b
cout << "v1.x == " << ( *v1.operator->() ).x << endl;  // §c

La salida indica que estas tres formas sí implican la invocación de la función operator->

Invocada funcion operator->()
v1.x == 2
Invocada funcion operator->()
v1.x == 2
Invocada funcion operator->()
v1.x == 2

Ya sabemos que le forma  §a es transformada por el compilador en la forma  §b, por lo que en realidad se trata de tres invocaciones explícitas a la función operator->. Observe que §a, §b y §c son equivalentes, y representan variaciones sintácticas para referirse al elemento v1.x.

§5  Punteros inteligentes

Debemos resaltar que en el programa anterior disponemos de dos formas de acceso indirecto a los miembros del objeto v1:

cout << v1->x;     // §a
cout << vptr->x;   // §d


Hemos indicado que ambas utilizan la versión global del selector -> sobre el miembro x como segundo operando. Pero existe una diferencia crucial: la forma  §a permite introducir una función previa, representada por operator->( ), lo que abre todo un mundo de posibilidades.

En realidad, este comportamiento atípico de la función operator->( ), que hemos visto se aparta del resto de operadores, no es arbitraria. Representa la puerta de acceso a lo que se denominan punteros inteligentes; objetos que actúan como punteros, pero que además pueden realizar alguna acción previa cada vez que un objeto es accedido a través de ellos. Habida cuenta que esta acción previa puede ser cualquiera (todo lo que pueda hacer una función), los punteros inteligentes permiten técnicas de programación muy interesantes.

La idea puede ser concretada en tres formas básicas que comentamos separadamente:

  1. Incluir la función operator-> en la definición de la clase
  2. Incluir la función operator-> en una clase independiente
  3. Incluir la función oprator-> en una clase anidada


§5.1
  Incluir la función operator->( ) en la definición de la clase (es el caso del ejemplo anterior):

class Vector {
  public: int x, y;
  Vector* operator-> () {
    /* funcionalidad adicional requerida */
    return this;
  }
};

Como se ha visto, este diseño permite que los objetos de la clase Vector puedan ser accedidos indirectamente:

Vector v1;
v1->x = 2;  v1->y = 4;


§5.2
  Incluir la función operator->( ) en una clase Vptr independiente:

class Vector {

  ...
};

 

class Vptr {

  ...

  Vector* vpt;

  Vector* operator->() {

    /* funcionalidad adicional requerida */

    return vpt;

  }

};


En este caso los objetos Vptr pueden ser utilizados para acceder a los de clase Vector, de forma parecida a como se utilizan los punteros.  Lo ilustramos con un ejemplo compilable:

#include <iostream>
using namespace std;

class Vector { public: int x, y; };

class Vptr {
  public:
  class Vector* vpt;
  Vector* operator->() {
    cout << "Acceso a vector (" << vpt->x << ", " << vpt->y << ")" << endl;
    return vpt;
  }
};

void main() {            // =====================
  Vector v1 = {2, 1};    // objeto tipo vector inicializado
  Vptr vpt = { &v1 };    // objeto Vptr que señala a v1
  cout << "v1.x == " << vpt->x << endl;
  vpt->y = 4;
  cout << "v1.y == " << vpt->y << endl;
}

Salida:

Acceso a vector (2, 1)
v1.x == 2
Acceso a vector (2, 1)
Acceso a vector (2, 4)
v1.y == 4


§5.3  Incluir la función operator->( ) en una clase Vptr contenida (anidada) en Vector:

class Vector {

  ...
  class Vptr {

    ...

    Vector* operator->() {

      /* funcionalidad adicional requerida */

      return vpt;

    }

  };

};


Si los objetos Vptr no tienen sentido como entes independientes de los objetos Vector, o no pueden ser utilizados para otras clases, esta disposición también puede ser válida y en cierta forma equivalente al diseño anterior.

Veamos este diseño en un ejemplo concreto:

#include <iostream>
using namespace std;

class Vector {
  public: int x, y;
  class Vptr {
    public:
    Vector* vpt;
    Vector* operator-> () {
      cout << "Accedido vector (" << vpt->x << ", " << vpt->y << ")" << endl;
      return vpt;
    }
  } p1;
};

void main() {    // ==============
  Vector v1 = {1, 2};
  v1.p1.vpt = &v1;                         // M.2
  cout << "v1.x == " << v1.p1->x << endl;  // M.3
  v1.p1->y = 4;
  cout << "v1.y == " << v1.p1->y << endl;
}

Como puede suponerse, la salida que se obtiene es idéntica a la del ejemplo anterior.

Observe en M.2 la inicialización del puntero vpt; un miembro del objeto p1. (miembro a su vez del objeto v1). Esta circunstancia, miembros de objetos que pueden tener a su vez propiedades y métodos, es característica de las clases cuyos miembros son a su vez instancias de otras clases. En este caso, de la clase Vptr definida dentro de Vector.

Observe también que en M.3 y siguientes se invoca el método operator-> del miembro p1 del objeto v1.


§5.3.2  Aunque la versión anterior es totalmente funcional, su diseño nos obliga a inicializar el puntero vpt para cada instancia de la clase Vector (como se ha hecho en M.2). En el ejemplo que sigue refinamos ligeramente el diseño anterior añadiéndole un constructor, al objeto de evitar tener que realizar esta inicialización cada vez.

#include <iostream>
using namespace std;

class Vector {
  public: int x, y;
  class Vptr {
    public:
    Vector* vpt;
    Vector* operator-> () {
      cout << "Accedido vector (" << vpt->x << ", " << vpt->y << ")" << endl;
      return vpt;
    }
  } p1;
  Vector (int i = 0, int j = 0) {  // L.14: constructor
    x = i; y = j;
    p1.vpt = this;
  }
};
 
void main() {    // ================
  Vector v1 = Vector(2, 1);             // M.1
  // v1.p1.vpt = &v1;  ya no es necesaria  M.2
  cout << "v1.x == " << v1.p1->x << endl;
  v1.p1->y = 4;
  cout << "v1.y == " << v1.p1->y << endl;
}

La salida es también idéntica a la de los dos anteriores. El cambio introducido se limita casi exclusivamente a la inclusión de un constructor explícito (L.14) para la clase Vector.

Como puede comprobarse, la inicialización M.2 ya no es necesaria; se ha encomendado esta función al constructor. Nótese la utilización explícita del puntero this ( 4.11.6) para este cometido.

Observe también que la existencia de un constructor explícito nos ha obligado a modificar ligeramente la sintaxis de la sentencia M.1 en la que creamos e inicializamos el objeto v1 ( 4.11.2d3).

En cualquier caso, cualquiera que sea el diseño adoptado para la "sobrecarga" del selector indirecto, debemos conservar la idea central: estos punteros inteligentes permiten la utilización de un código cuya ejecución es previa al acceso al objeto. En los ejemplos anteriores dicho código se ha concretado en una salida mostrando el estado actual (componentes) del vector.

  Inicio.


[1]  Como veremos a continuación, bajo cierto punto de vista puede ser considerado un operador unario.

[2]  Por supuesto que nada impide que dicha función sea invocada al modo tradicional, aunque en este caso la sintaxis no es obj.operator-> sino obj.operator->( ). Ver una muestra de esta utilización en el ejemplo adjunto .

[3]  Incluyendo el libro de Strousturp TC++PL  §11.10