11.4. Punteros a miembros

Un puntero es una variable que contiene la dirección de alguna ubicación. Se puede cambiar a lo que el puntero apunta en tiempo de ejecución. La ubicación a la que apunta puede ser un dato o función. El puntero a miembro de C++ sigue el mismo concepto, excepto que a lo que apunta es una ubicación dentro de una clase. Pero surge el dilema de que un puntero necesita una dirección, pero no hay «dirección» alguna dentro de una clase; La selección de un miembro de una clase se realiza mediante un desplazamiento dentro de la clase. Pero primero hay que conocer la dirección donde comienza un objeto en particular para luego sumarle el desplazamiento y así localizar el miembro de la clase. La sintaxis de los punteros a miembros requiere que usted seleccione un objeto al mismo tiempo que está accediendo al contenido del puntero al miembro.

Para entender esta sintaxis, considere una estructura simple, con un puntero sp y un objeto so. Puede seleccionar sus miembros de la misma manera que en el siguiente ejemplo:

//: C11:SimpleStructure.cpp
struct Simple { int a; };
int main() {
  Simple so, *sp = &so;
  sp->a;
  so.a;
} ///:~

Listado 11.11. C11/SimpleStructure.cpp


Ahora suponga que tiene un puntero normal que se llama ip y que apunta a un entero. Para acceder a lo que apunta ip, ha de estar precedido por un '*':

*ip=4;

Finalmente, se preguntará qué pasa si tiene un puntero que está apuntando a algo que está dentro de un objeto, incluso si lo que realmente representa es un desplazamiento dentro del objeto. Para acceder a lo que está apuntando, debe preceder el puntero con '*'. Pero como es un desplazamiento dentro de un objeto, también ha de referirse al objeto con el que estamos tratando. Así, el * se combina con el objeto. Por tanto, la nueva sintaxis se escribe ->* para un puntero que apunta a un objeto, y .* para un objeto o referencia, tal como esto:

objectPointer->*pointerToMember = 47;
object.*pointerToMember = 47;

Pero, ¿cuál es la sintaxis para definir el pointerToMember? Pues como cualquier puntero, tiene que decir el tipo al que apuntará, por lo que se utilizaría el '*' en la definición. La única diferencia es que debe decir a qué clase de objetos apuntará ese atributo puntero. Obviamente, esto se consigue con el nombre de la clase y el operador de resolución de ámbito. Así,

int ObjectClass::*pointerToMember;

define un atributo puntero llamado pointerToMember que apunta a cualquier entero dentro de ObjectClass. También puede inicializar el puntero cuando lo defina (o en cualquier otro momento):

int ObjectClass::*pointerToMember = &ObjectClass::a;

Realmente no existe una «dirección» de ObjectClass::a porque se está refiriendo a la clase y no a un objeto de esa clase. Así, &ObjectClass::a se puede utilizar sólo con la sintaxis de un puntero a miembro.

He aquí un ejemplo que muestra cómo crear y utilizar punteros a atributos:

//: C11:PointerToMemberData.cpp
#include <iostream>
using namespace std;

class Data {
public:  
  int a, b, c; 
  void print() const {
    cout << "a = " << a << ", b = " << b
         << ", c = " << c << endl;
  }
};

int main() {
  Data d, *dp = &d;
  int Data::*pmInt = &Data::a;
  dp->*pmInt = 47;
  pmInt = &Data::b;
  d.*pmInt = 48;
  pmInt = &Data::c;
  dp->*pmInt = 49;
  dp->print();
} ///:~

Listado 11.12. C11/PointerToMemberData.cpp


Obviamente, son muy desagradables de utilizar en cualquier lugar excepto para caso especiales (que es exactamente para lo que se crearon).

Además, los punteros a miembro son bastante limitados: pueden asignarse solamente a una ubicación específica dentro de una clase. No podría, por ejemplo, incrementarlos o compararlos tal como puede hacer con punteros normales.

11.4.1. Funciones

Un ejercicio similar se produce con la sintaxis de puntero a miembro para métodos. Un puntero a una función (presentado al final del Capítulo 3) se define como:

int (*fp)(float);

Los paréntesis que engloban a (*fb) son necesarios para que fuercen la evaluación de la definición apropiadamente. Sin ellos sería una función que devuelve un int*.

Los paréntesis también desempeñan un papel importante cuando se definen y utilizan punteros a métodos. Si tiene una función dentro de una clase, puede definir un puntero a ese método insertando el nombre de la clase y el operador de resolución de ámbito en una definición habitual de puntero a función:

//: C11:PmemFunDefinition.cpp
class Simple2 { 
public: 
  int f(float) const { return 1; }
};
int (Simple2::*fp)(float) const;
int (Simple2::*fp2)(float) const = &Simple2::f;
int main() {
  fp = &Simple2::f;
} ///:~

Listado 11.13. C11/PmemFunDefinition.cpp


En la definición de fp2 puede verse que el puntero a un método puede inicializarse cuando se crea, o en cualquier otro momento. A diferencia de las funciones no son miembros, el & no es opcional para obtener la dirección de un método. Sin embargo, se puede dar el identificador de la función sin la lista de argumentos, porque la sobrecarga se resuelve por el tipo de puntero a miembro.

Un ejemplo

Lo interesante de un puntero es que se puede cambiar el valor del mismo para apuntar a otro lugar en tiempo de ejecución, lo cual proporciona mucha flexibilidad en la programación porque a través de un puntero se puede cambiar el comportamiento del programa en tiempo de ejecución. Un puntero a miembro no es distinto; le permite elegir un miembro en tiempo de ejecución. Típicamente, sus clases sólo tendrán métodos visibles públicamente (los atributos normalmente se consideran parte de la implementación que va oculta), de modo que el siguiente ejemplo elige métodos en tiempo de ejecución.

//: C11:PointerToMemberFunction.cpp
#include <iostream>
using namespace std;

class Widget {
public:
  void f(int) const { cout << "Widget::f()\n"; }
  void g(int) const { cout << "Widget::g()\n"; }
  void h(int) const { cout << "Widget::h()\n"; }
  void i(int) const { cout << "Widget::i()\n"; }
};

int main() {
  Widget w;
  Widget* wp = &w;
  void (Widget::*pmem)(int) const = &Widget::h;
  (w.*pmem)(1);
  (wp->*pmem)(2);
} ///:~

Listado 11.14. C11/PointerToMemberFunction.cpp


Por supuesto, no es razonable esperar que el usuario casual cree expresiones tan complejas. Si el usuario necesita manipular directamente un puntero a miembro, los typedef vienen al rescate. Para dejar aún mejor las cosas, puede utilizar un puntero a función como parte del mecanismo interno de la implementación. He aquí un ejemplo que utiliza un puntero a miembro dentro de la clase. Todo lo que el usuario necesita es pasar un número para elegir una función. [69]

//: C11:PointerToMemberFunction2.cpp
#include <iostream>
using namespace std;

class Widget {
  void f(int) const { cout << "Widget::f()\n"; }
  void g(int) const { cout << "Widget::g()\n"; }
  void h(int) const { cout << "Widget::h()\n"; }
  void i(int) const { cout << "Widget::i()\n"; }
  enum { cnt = 4 };
  void (Widget::*fptr[cnt])(int) const;
public:
  Widget() {
    fptr[0] = &Widget::f; // Full spec required
    fptr[1] = &Widget::g;
    fptr[2] = &Widget::h;
    fptr[3] = &Widget::i;
  }
  void select(int i, int j) {
    if(i < 0 || i >= cnt) return;
    (this->*fptr[i])(j);
  }
  int count() { return cnt; }
};

int main() {
  Widget w;
  for(int i = 0; i < w.count(); i++)
    w.select(i, 47);
} ///:~

Listado 11.15. C11/PointerToMemberFunction2.cpp


En la interfaz de la clase y en main(), puede observar que toda la implementación, funciones incluidas, es privada. El código ha de pedir el count() de las funciones. De esta manera, el que implementa la clase puede cambiar la cantidad de funciones en la implementación por debajo sin que afecte al código que utilice la clase.

La inicialización de los punteros a miembro en el constructor puede parecer redundante. ¿No debería ser capaz de poner

fptr[1] = &g;

porque el nombre g es un método, el cual está en el ámbito de la clase? El problema aquí es que no sería conforme a la sintaxis de puntero a miembro. Así todo el mundo, incluido el compilador, puede imaginarse qué está pasando. De igual forma, cuando se accede al contenido del puntero a miembro, parece que

(this->*fptr[i])(j);

también es redundante; this parece redundante. La sintaxis necesita que un puntero a miembro siempre esté ligado a un objeto cuando se accede al contenido al que apunta.



[69] Gracias a Owen Mortensen por este ejemplo