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 sobre el que se invoca actúa como un array, a menudo devolverá una referencia desde este operador, de modo que se pueda usar cómodamente en la parte izquierda 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 Capítulo 13.
El operador coma se llama cuando aparece junto a 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 que aparecen sueltos, separados por comas. No parece haber un montón de usos prácticos para este operador; está en el lenguaje por coherencia. He aquí un ejemplo que muestra cómo 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
La función global permite situar la coma antes del objeto en cuestión. El uso mostrado es bastante oscuro y cuestionable. Aunque probablemente usaría una lista separada por comas como parte de una expresión más complicada, es demasiado sutil para usarlo en la mayoría de las situaciones.
El operador -> se usa generalmente cuando quiere hacer que un objeto parezca un puntero. Como un objeto así tiene más «inteligencia» que un puntero normal, se le suele llamar puntero inteligente o más a menudo por su equivalente en inglés: smart pointer. Resultan especialmente útiles si quiere «envolver» un puntero con una clase para hacer que ese puntero sea seguro, o en el uso habitual 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, 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 Librería 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 indirecció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 atributos estáticos. Los punteros a estos objetos son almacenados en el interior de los contenedores del tipo ObjContainer usando su función add(). ObjContainer parece un array de punteros, pero advertirá que no hay forma de volver a sacar 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 indirección de punteros) el valor al que apunta. Advierta que SmartPointer está hecho a medida para 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 capítulo de este libro y en el Volumen 2 (descargable desde www.BruceEckel.com).
En main(), una vez que el contenedor oc se rellena con objetos Obj se crea un SmartPointer sp. Las llamadas al puntero inteligente se producen 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 el Obj* que devuelve 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 una 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 está en el método begin() de ObjContainer, que produce un SmartPointer que apunta al principio de la secuencia del ObjContainer. Aunque realmente es sólo por conveniencia, es valioso porque sigue en parte la forma usada en 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 capítulo anterior.
Igual que operator->, el operador de indirección de puntero a miembro se usa normalmente con algún tipo de objeto que representa 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.
El operador de llamada a función operator() debe ser un método, y es único en cuanto 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 sólo tienen una única operación, o al menos una especialmente destacada. En el Volumen 2 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 llamado (lo que sucede automáticamente), el puntero a miembro sea dereferenciado 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 mediante el 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. PMF es un typedef para simplificar la definición de un puntero a miembro para los métodos de Dog.
Un FunctionObject es creado y devuelto por operator->*. Fíjese en que operator->* conoce el objeto para el que el puntero a miembro está siendo llamado (this) y el puntero a miembro, y los pasa al constructor de FunctionObject, que almacena esos valores. Cuando se llama a operator->*, el compilador llama inmediatamente a operator() para el valor de retorno de operator->*, pasándole los argumentos que le fueron pasados a operator->*. FunctionObject::operator() toma los argumentos y dereferencia el puntero a miembro «real» usando los punteros a objeto y a miembro almacenados.
Fíjese en que lo que está haciendo aquí, igual que con operator->, es insertarse en mitad de la llamada a operator->*. Esto le permite realizar algunas operaciones adicionales si lo necesita.
El mecanismo operator->* implementado aquí sólo funciona con 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 en el último capítulo de este libro, y en el Volumen 2) está diseñado para manejar semejante problema.