Publi

C++ Punteros a función miembro (pointer-to-member function) o callbacks con clase


Otra de las características que dan a C++ mucha más flexibilidad son los punteros a miembro. Éstos nos permiten trabajar con elementos que podemos encontrar dentro de una clase, un struct o una union. Y el mejor ejemplo lo encontramos a la hora de hacer callbacks, si repasamos los callbacks en C inmediatamente veremos que en un lenguaje orientado a objetos necesitamos poder acceder a métodos dentro de una clase. Si hacemos un ejemplo parecido al de aquel post, podemos demostrar que la misma manera podemos acceder a métodos estáticos dentro de la clase:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#include <iostream>
#include <cstring>

using namespace std;

/* TYPEDEF */
typedef int (*funcion_callback)(char*);

class MyClass
{
public:
  static int eco(char *cadena)
  {
    cout << "Eco: "<<cadena<<endl;
    return (strlen(cadena));
  }
};

/* DECLARACIÓN DE LLAMADORA */
void generaecos(funcion_callback func)
{
  int num;
  cout << "Voy a llamar al eco con el texto HOLA" << endl;
  num=func((char*)"HOLA");
  cout << "La he llamado y me ha devuelto: "<< num << endl;
}

int main()
{
  generaecos(MyClass::eco);
}

Pero cuando queremos llamar a un método no estático de la clase fallará sin miramientos, ya que necesitaríamos la información del objeto en el que se encuentra el método, y de esta forma estaríamos pasando directamente una dirección de memoria (al ejecutar la función no encontraría el puntero this, entre otras cosas).
Por lo tanto debemos crear este tipo de punteros especiales, podemos considerar que almacenan una dirección relativa del miembro dentro de la clase que la contiene, es más, no necesitamos una instancia a la hora de asignar el método que queremos introducir, sólo a la hora de llamarlo:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
#include <iostream>

using namespace std;

class MyClass
{
public:
  MyClass();
  ~MyClass();
  void saludo(char *nombre);
};

MyClass::MyClass()
{
  cout << "Inicializamos la clase" << endl;
}

MyClass::~MyClass()
{
  cout << "Destruimos la instancia" << endl;
}

void MyClass::saludo(char *nombre)
{
  cout << "Hola "<<nombre<<", ¿ćomo estas?" << endl;
}

/* TYPEDEF */
typedef void (MyClass::*MyCallback)(char*);

int main()
{
  MyCallback cb = &MyClass::saludo; // La instancia no existe aún
  MyClass c;

  (c.*cb)((char*)"poeta");
}

Lo único que debemos hacer es declarar el puntero (está bien crear un typedef como el del ejemplo si lo vamos a utilizar varias veces), luego debemos darle un valor que será &Clase::método, sin pasarle ningún objeto, luego a la hora de llamarlo, debemos poner

(instancia.*puntero)(argumentos)

o si la instancia es dinámica

(instancia->*puntero)(argumentos)

Eso sí, si vamos a llamar a un método desde fuera de la clase, éste debe ser un método público (por supuesto), aunque, si asignamos el valor del callback desde dentro de la clase, podremos acceder a métodos privados:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
#include <iostream>

using namespace std;

class MyClass
{
public:
  typedef void (MyClass::*Cb)(char*);

  MyClass();
  ~MyClass();
  void saludo(char *nombre);

  Cb callb;
private:
  void saludo2(char *nombre);  
};

MyClass::MyClass()
{
  cout << "Inicializamos la clase" << endl;
  callb = &MyClass::saludo2;
}

MyClass::~MyClass()
{
  cout << "Destruimos la instancia" << endl;
}

void MyClass::saludo(char *nombre)
{
  cout << "Hola "<<nombre<<", ¿ćomo estas?" << endl;
}

void MyClass::saludo2(char *nombre)
{
  cout << "Saludo 2: Buenos días señor/a "<<nombre<<", ¿ćomo estas?" << endl;
}


int main()
{
  MyClass c;

  MyClass::Cb cb2 = c.callb;

  (c.*cb2)((char*)"poeta");
}

Ahora bien, vamos a jugar un poco con estas propiedades, ¿qué pasaría si intento llamar a una función de parte de otra clase? La clase no se inicializa como debería, aunque sí se ejecutará el método bueno, en principio si el método (aunque sea dinámico, no utiliza atributos de la clase, no pasaría nada, pero es una práctica un tanto arriesgada:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
#include <iostream>
#include <string>

using namespace std;

class MyClass
{
public:
  typedef void (MyClass::*Cb)(char*);

  MyClass();
  ~MyClass();
  void saludo(char *nombre);

  int valor;
  string hola;
private:
};

MyClass::MyClass()
{
  valor=5;
  cout << "Inicializamos la clase" << endl;
}

MyClass::~MyClass()
{
  cout << "Destruimos la instancia" << endl;
}

void MyClass::saludo(char *nombre)
{
  cout << "Hola "<<nombre<<", ¿ćomo estas?" << "Valor: "<< valor <<endl;
  //  hola="PROBABLEMENTE ESTO FALLE";
}

class OtherClass
{
public:

  OtherClass()
  {
  }

  virtual void saludo(char *nombre)
  {
    cout << "Simple saludo"<<endl;
  }

private:
  void saludo2(char *nombre)
  {
  }
};

int main()
{
  MyClass c;
  OtherClass o;

  MyClass::Cb cb2 = &MyClass::saludo;

  (o.*(void (OtherClass::*)(char*))cb2)((char*)"poeta"); // Podemos usar   (o.*reinterpret_cast<void (OtherClass::*)(char*)>(cb2))((char*)"poeta");
}

Otro tema es la utilización de métodos virtuales, lo cual puede ser muy útil cuando estamos utilizando clases derivadas, aunque también podemos utilizar si la clase que vamos a utilizar tiene la misma forma que la original, para ello, al declarar el método saludo() en la clase MyClass, podemos poner virtual delante, y compilar, en este caso se ejecutará el método saludo de OtherClass,
Pero si ahora, le cambiamos el nombre al método de OtherClass (cualquier otro nombre que no sea saludo()), también se ejecutará el método.

El gran problema, puede ser que para cada método, hace falta que creemos el prototipo con su tipo de salida y sus argumentos, cosa que se puede volver muy dura si tenemos muchos posibles métodos a los que llamar, debemos recordar también, que esto es C++, no podemos hacer lo mismo que en un lenguaje interpretado que nos puede dar mucha más flexibilidad en ciertos aspectos.

De todas formas, dejo para una segunda parte del artículo un ejemplo práctico de todo esto con clases heredadas donde podremos ver gran utilidad y flexibilidad.

Foto: Maryam (Flickr) CC-by a 12/06/2013

También podría interesarte...

There are 2 comments left Ir a comentario

  1. Pingback: Bitacoras.com /

  2. Pingback: Callbacks en C++ con ejemplos utilizando Boost /

Leave a Reply