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:
- Incluir la función operator-> en la definición de la clase
- Incluir la función operator-> en una clase independiente
- 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.
[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 .