12.3.4. Operadores poco usuales

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

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

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.

Un operador anidado

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

Operador ->*

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.