Varios operadores adicionales tienen una forma ligeramente diferente de ser sobrecargados.
El subíndice, operator[]
debe ser un método y precisa de un
único argumento. Dado que operator[]
implica que el objeto
que está siendo utilizado como un array, a menudo devolverá una referencia de este
operador, así que puede ser convenientemente usado en la parte derecha de un signo
de igualdad. Este operador es muy comúnmente sobrecargado; verá ejemplos en el
resto del libro.
Los operadores new
y delete
controlan la reserva dinámica de
almacenamiento y se pueden sobrecargar de muchas maneras diferentes. Este tema se
cubre en el capitulo 13.
El operador coma se llama cuando aparece después de un objeto del tipo para el
que está definido. Sin embargo, «operator,
» no
se llama para listas de argumentos de funciones, sólo para objetos fuera de ese
lugar separados por comas. No parece haber un montón de usos prácticos para este
operador, solo es por consistencia del lenguaje. He aquí un ejemplo que muestra
como la función coma se puede llamar cuando aparece antes de un objeto, así como
después:
//: C12:OverloadingOperatorComma.cpp #include <iostream> using namespace std; class After { public: const After& operator,(const After&) const { cout << "After::operator,()" << endl; return *this; } }; class Before {}; Before& operator,(int, Before& b) { cout << "Before::operator,()" << endl; return b; } int main() { After a, b; a, b; // Operator comma called Before c; 1, c; // Operator comma called } ///:~
Listado 12.8. C12/OverloadingOperatorComma.cpp
Las funciones globales permiten situar la coma antes del objeto en cuestión. El uso mostrado es bastante oscuro y cuestionable. Probablemente podría una lista separada por comas como parte de una expresión más complicada, es demasiado refinado en la mayoría de las ocasiones.
El operador ->
se usa generalmente cuando quiere hacer que un
objeto parezca un puntero. Este tipo de objeto se suele llamar puntero
inteligente o más a menudo por su equivalente en inglés:
smart pointer. Resultan especialmente utiles si quiere
«envolver» una clase con un puntero para hacer que ese puntero sea
seguro, o en la forma común de un iterador
, que es un
objeto que se mueve a través de una colección
o
contenedor
de otros objetos y los selecciona de uno en
uno cada vez, sin proporcionar acceso directo a la implementación del
contenedor. (A menudo encontrará iteradores y contenedores en las librerías de
clases, como en la Biblioteca Estándar de C++, descrita en el volumen 2 de este
libro).
El operador de indirección de punteros (*) debe ser un método. Tiene otras restricciones atípicas: debe devolver un objeto (o una referencia a un objeto) que también tenga un operador de indirección de punteros, o debe devolver un puntero que pueda ser usado para encontrar a lo que apunta la flecha del operador de indireción de punteros. He aquí un ejemplo simple:
//: C12:SmartPointer.cpp #include <iostream> #include <vector> #include "../require.h" using namespace std; class Obj { static int i, j; public: void f() const { cout << i++ << endl; } void g() const { cout << j++ << endl; } }; // Static member definitions: int Obj::i = 47; int Obj::j = 11; // Container: class ObjContainer { vector<Obj*> a; public: void add(Obj* obj) { a.push_back(obj); } friend class SmartPointer; }; class SmartPointer { ObjContainer& oc; int index; public: SmartPointer(ObjContainer& objc) : oc(objc) { index = 0; } // Return value indicates end of list: bool operator++() { // Prefix if(index >= oc.a.size()) return false; if(oc.a[++index] == 0) return false; return true; } bool operator++(int) { // Postfix return operator++(); // Use prefix version } Obj* operator->() const { require(oc.a[index] != 0, "Zero value " "returned by SmartPointer::operator->()"); return oc.a[index]; } }; int main() { const int sz = 10; Obj o[sz]; ObjContainer oc; for(int i = 0; i < sz; i++) oc.add(&o[i]); // Fill it up SmartPointer sp(oc); // Create an iterator do { sp->f(); // Pointer dereference operator call sp->g(); } while(sp++); } ///:~
Listado 12.9. C12/SmartPointer.cpp
La clase Obj
define los objetos que son manipulados en
este programa. Las funciones f()
y g()
simplemente escriben en pantalla los valores interesantes usando miembros de
datos estáticos. Los punteros a estos objetos son almacenados en el interior de
los contenedores del tipo ObjContainer
usando su función
add()
. ObjContanier
parece un array
de punteros, pero advertirá que no hay forma de traer de nuevo los punteros. Sin
embargo, SmartPointer
se declara como una clase
friend
, así que tiene permiso para mirar dentro del
contenedor. La clase SmartPointer
se parece mucho a un
puntero inteligente - puede moverlo hacia adelante usando
operator++
(también puede definir un
operator--
, no pasará del final del contenedor al que
apunta, y genera (a través del operador de indireccion de punteros) el valor al
que apunta. Advierta que SmartPointer
está hecho a medida
sobre el contenedor para el que se crea; a diferencia de un puntero normal, no
hay punteros inteligentes de «propósito general». Aprenderá más
sobre los punteros inteligentes llamados «iteradores» en el último
capitulo de este libro y en el volumen 2 (descargable desde FIXME:url
www. BruceEckel. com).
En main()
, una vez que el contenedor oc
se rellena con objetos Obj
se crea un SmartPointer
sp
. La llamada al puntero inteligente sucede en las expresiones:
sp->f(); // Llamada al puntero inteligente sp->g();
Aquí, incluso aunque sp
no tiene métodos
f()
y g()
, el operador de indirección
de punteros automáticamente llama a esas funciones para Obj*
que es
devuelto por SmartPointer::operator->
. El compilador
realiza todas las comprobaciones pertinentes para asegurar que la llamada a
función funciona de forma correcta.
Aunque la mecánica subyacente de los operadores de indirección de punteros es más compleja que la de los otros operadores, el objetivo es exactamente el mismo: proporcionar una sintaxis más conveniente para los usuarios de sus clases.
Es más común ver un puntero inteligente o un clase iteradora anidada dentro de
la clase a la que sirve. Se puede reescribir el ejemplo anterior para anidar
SmartPointer
dentro de
ObjContainer
así:
//: C12:NestedSmartPointer.cpp #include <iostream> #include <vector> #include "../require.h" using namespace std; class Obj { static int i, j; public: void f() { cout << i++ << endl; } void g() { cout << j++ << endl; } }; // Static member definitions: int Obj::i = 47; int Obj::j = 11; // Container: class ObjContainer { vector<Obj*> a; public: void add(Obj* obj) { a.push_back(obj); } class SmartPointer; friend class SmartPointer; class SmartPointer { ObjContainer& oc; unsigned int index; public: SmartPointer(ObjContainer& objc) : oc(objc) { index = 0; } // Return value indicates end of list: bool operator++() { // Prefix if(index >= oc.a.size()) return false; if(oc.a[++index] == 0) return false; return true; } bool operator++(int) { // Postfix return operator++(); // Use prefix version } Obj* operator->() const { require(oc.a[index] != 0, "Zero value " "returned by SmartPointer::operator->()"); return oc.a[index]; } }; // Function to produce a smart pointer that // points to the beginning of the ObjContainer: SmartPointer begin() { return SmartPointer(*this); } }; int main() { const int sz = 10; Obj o[sz]; ObjContainer oc; for(int i = 0; i < sz; i++) oc.add(&o[i]); // Fill it up ObjContainer::SmartPointer sp = oc.begin(); do { sp->f(); // Pointer dereference operator call sp->g(); } while(++sp); } ///:~
Listado 12.10. C12/NestedSmartPointer.cpp
Además del anidamiento de la clase, hay solo dos diferencias aquí. La primera es
la declaración de la clase para que pueda ser friend
:
class SmartPointer; friend SmartPointer;
El compilador debe saber primero que la clase existe, antes de que se le diga que es «amiga».
La segunda diferencia es en ObjContainer
donde el método
begin()
produce el SmartPointer
que
apunta al principio de la secuencia del
ObjContainer
. Aunque realmente es sólo por conveniencia,
es adecuado porque sigue la manera habitual de la librería estándar de C++.
El operador ->*
es un operador binario que se comporta como todos
los otros operadores binarios. Se proporciona para aquellas situaciones en las
que quiera imitar el comportamiento producido por la sintaxis incorporada
puntero a miembro, descrita en el capitulo anterior.
Igual que operator->
, el operador de indirección de puntero a
miembro se usa normalmente con alguna clase de objetos que representan un
«puntero inteligente», aunque el ejemplo mostrado aquí será más
simple para que sea comprensible. El truco cuando se define
operator->*
es que debe devolver un objeto para el que
operator()
pueda ser llamado con los argumentos para la
función miembro que usted llama.
La llamada a función operator()
debe ser un método, y es
único en que permite cualquier número de argumentos. Hace que el objeto parezca
realmente una función. Aunque podría definir varias funciones sobrecargadas
operator()
con diferentes argumentos, a menudo se usa para
tipos que solo tienen una operación simple, o al menos una especialmente
destacada. En el Volumen2 verá que la Librería Estándar de C++ usa el operador
de llamada a función para crear «objetos-función».
Para crear un operator->*
debe primero crear una clase con un
operator()
que sea el tipo de objeto que
operator->*
devolverá. Esta clase debe, de algún modo, capturar la
información necesaria para que cuando operator()
sea
llamada (lo que sucede automáticamente), el puntero a miembro sea indireccionado
para el objeto. En el siguiente ejemplo, el constructor de
FunctionObject
captura y almacena el puntero al objeto y
el puntero a la función miembro, y entonces operator()
los
usa para hacer la verdadera llamada puntero a miembro:
//: C12:PointerToMemberOperator.cpp #include <iostream> using namespace std; class Dog { public: int run(int i) const { cout << "run\n"; return i; } int eat(int i) const { cout << "eat\n"; return i; } int sleep(int i) const { cout << "ZZZ\n"; return i; } typedef int (Dog::*PMF)(int) const; // operator->* must return an object // that has an operator(): class FunctionObject { Dog* ptr; PMF pmem; public: // Save the object pointer and member pointer FunctionObject(Dog* wp, PMF pmf) : ptr(wp), pmem(pmf) { cout << "FunctionObject constructor\n"; } // Make the call using the object pointer // and member pointer int operator()(int i) const { cout << "FunctionObject::operator()\n"; return (ptr->*pmem)(i); // Make the call } }; FunctionObject operator->*(PMF pmf) { cout << "operator->*" << endl; return FunctionObject(this, pmf); } }; int main() { Dog w; Dog::PMF pmf = &Dog::run; cout << (w->*pmf)(1) << endl; pmf = &Dog::sleep; cout << (w->*pmf)(2) << endl; pmf = &Dog::eat; cout << (w->*pmf)(3) << endl; } ///:~
Listado 12.11. C12/PointerToMemberOperator.cpp
Dog
tiene tres métodos, todos toman un argumento entero y
devuelven un entero. PMC
es un typedef
para
simplificar la definición de un puntero a miembro para los métodos de
Dog
.
Una FunctionObject
es creada y devuelta por
operator->*
. Dese cuenta que operator->*
conoce el
objeto para el que puntero a miembro está siendo llamado
(this
) y el puntero a miembro, y los pasa al constructor
FunctionObject
que almacena sus valores. Cuando se llama a
operator->*
, el compilador inmediatamente lo revuelve y llama a
operator()
para el valor de retorno de
operator->*
, pasándole los argumentos que le fueron pasados a
operator->*
. FunctionObject::operator()
toma
los argumentos e desreferencia el puntero a miembro «real» usando
los punteros a objeto y a miembro almacenados.
Percátese de que lo que está ocurriendo aquí, justo como con
operator->
, se inserta en la mitad de la llamada a
operator->*
. Esto permite realizar algunas operaciones adicionales
si se necesita.
El mecanismo operator->*
implementado aquí solo trabaja para
funciones miembro que toman un argumento entero y devuelven otro entero. Esto es
una limitación, pero si intenta crear mecanismos sobrecargados para cada
posibilidad diferente, verá que es una tarea prohibitiva. Afortunadamente, el
mecanismo de plantillas de C++ (descrito el el ultimo capitulo de este libro, y
en el volumen2) está diseñado para manejar semejante problema.