Publi

Callbacks en C++11, llamando a métodos con un objeto asociado (II)

2747581103_a6c79b8a38_o

Hace dos semanas hablábamos de Callbacks en C++11 nuevas posibilidades para un software más potente . Empezamos con una pequeña introducción para “almacenar” una función en una variable o un argumento de función y llamarla desde ahí, incluso introdujimos las funciones anónimas o lambdas.

Ahora, como no podía ser de otra forma, y dado que estamos en un lenguaje orientado a objetos, en el que queremos aprovechar todo su potencial. Vamos a hacer varios ejemplos en los que llamaremos a métodos de una clase de varias formas diferentes.

Llamando a un método de una clase cambiando el objeto

Vamos a empezar fuerte, aunque el ejemplo sea muy largo para lo que es, el objetivo es que nuestra función apunta a un método de una clase pero, a la hora de llamarlo, elegimos sobre qué objeto aplicar esa llamada. Para complicarlo un poco más, los objetos pertenecerán a clases derivadas (Suma y Multiplica) de una clase padre (Opera).

Es decir, el método al que hago referencia (computa) es de Opera, pero como tanto Suma como Multiplica heredan de dicha clase, podremos decirle a los objetos derivados de esas clases que llamen a computa.

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
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
#include <iostream>
#include <string>
#include <functional>

using namespace std;

class Opera
{
public:
  Opera(string id): id(id)
  {
  }

  virtual ~Opera()
  {
  }

  virtual string computa(string entrada) =0;
protected:
  string getId()
  {
    return id;
  }

private:
  string id;
};

class Suma : public Opera
{
public:
  Suma(string id): Opera(id)
  {
  }

  string computa(string entrada)
  {
    int res=0;
    size_t pos;
    while ((pos=entrada.find(' '))!=string::npos)
      {
    res+=stoi(entrada.substr(0, pos));
    entrada=entrada.substr(pos+1);
      }
    res+=stoi(entrada);
    return to_string(res);
  }
};

class Multiplica : public Opera
{
public:
  Multiplica(string id): Opera(id)
  {
  }

  string computa(string entrada)
  {
    int res=1;
    size_t pos;
    while ((pos=entrada.find(' '))!=string::npos)
      {
    res*=stoi(entrada.substr(0, pos));
    entrada=entrada.substr(pos+1);
      }
    res*=stoi(entrada);
    return to_string(res);
  }
};

int main()
{
  Suma s("uno_mas_uno");
  Multiplica m("dos por dos");

  function<string(Opera&, string)> myop = &Opera::computa;

  cout << "SUMA: "<<myop(s, "1 2 3 4")<<endl;
  cout << "MULTIPLICA: "<<myop(m, "1 2 3 4")<<endl;
  return 0;
}

Para compilar:

g++ -o operaciones operaciones.cpp -std=c++11

El método computa en la clase suma, cogerá cada número de la cadena y lo sumará, el método multiplica, hará lo propio también. Eso sí, a myop le hemos dicho que apunte a Opera::computa, como dijimos antes, estará presente en los objetos s y m. Pero, a la hora de llamarlo es cuando le decimos el objeto sobre el que tiene que hacer la operación.

Otra opción (cambiamos el main()) es que, al llamar al método, en lugar de utilizar el objeto (que pasaremos por referencia), utilicemos un puntero a ese objeto. En este código queda un poco feo, tenemos que andar sacando las referencias de los objetos (&):

1
2
3
4
5
6
7
8
9
10
11
int main()
{
  Suma s("uno_mas_uno");
  Multiplica m("dos por dos");

  function<string(Opera*, string)> myop = &Opera::computa;

  cout << "SUMA: "<<myop(&s, "1 2 3 4")<<endl;
  cout << "MULTIPLICA: "<<myop(&m, "1 2 3 4")<<endl;
  return 0;
}

Pero, si en nuestro caso ya tenemos las referencias, por ejemplo si los objetos los hemos instanciado con new, o si estamos definiendo estas llamadas desde la propia clase (es decir, el objeto es this), se vuelve un poco incómodo trabajar con *this todo el rato y nos viene bien hacer referencia a los objetos a través de un puntero.

Otro detalle más, como somos fans de auto, podemos utilizar lo siguiente:

1
2
3
4
5
6
7
8
9
10
11
int main()
{
  Suma s("uno_mas_uno");
  Multiplica m("dos por dos");

  auto myop = mem_fn(&Opera::computa);

  cout << "SUMA: "<<myop(&s, "1 2 3 4")<<endl;
  cout << "MULTIPLICA: "<<myop(&m, "1 2 3 4")<<endl;
  return 0;
}

Llamando a un método que ya tiene asociado un objeto

El hecho es que tenemos una función que apunta a un método de una clase y queremos que se ejecute en un objeto que ya tenemos instanciado. En el ejemplo, vemos cómo la función conecta() tiene dos parámetros: el primero es el servidor y el segundo es la función que llamaremos para guardar un informe, o un log de lo ocurrido, pero nosotros ya lo tenemos todo, al método no le tienes que proporcionar el objeto porque ya lo tenemos (imaginad que podemos hacer log en pantalla, en un fichero, enviarlo remotamente, etc).
En definitiva, la función sólo quiere llamar a una función, decirle el nivel de importancia de ese log, y el mensaje que quiere escribir. La función de log se podrá encargar de escribir la fecha y la hora, de gestionar el recurso donde imprime (por ejemplo, si es en un fichero, tendrá que bloquear el fichero para que nadie más escriba mientras él está escribiendo, y liberarlo cuando deje de escribir. + info).
Ahí va:

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
#include <iostream>
#include <string>
#include <functional>
#include <ctime>
#include <chrono>
#include <thread>

using namespace std;

struct Salida
{
  Salida(string nombre, int nivel):nombre(nombre), nivel(nivel)
  {
  }

  void escribe(int nivel, string mensaje)
  {
    if (nivel<this->nivel)
      return;

    string s(128, '\0');

    /* Vale, tenemos std::put_time() en C++11, pero yo uso GCC y hasta la
     versión 5.2 no está implementado, por lo que tengo que seguir usando
     las funciones de C. */

    std::time_t now = time(NULL);
    std::tm tm;
    localtime_r( &now, &tm );
    strftime( &s[0], s.size(), "%d/%m/%Y %h:%M:%S", &tm );
    cout << "[" << s << "] "<<nombre<<" Nivel: "<<nivel<<" | "<<mensaje<<endl;
  }
private:
  string nombre;
  int nivel;
};

void conecta(string servidor, function<void(int, string)> logger)
{
  logger(5, "Conectando al servidor "+servidor+"...");

  /* Hacemos como que conectamos */
  this_thread::sleep_for(chrono::seconds(1));

  logger(6, "Servidor "+servidor+" conectado!");
}

int main()
{
  Salida s("default", 5);

  conecta("totaki.com", std::bind(&Salida::escribe, &s, std::placeholders::_1, std::placeholders::_2));
  return 0;
}

La función conecta() no hace nada, lo sé, sólo espera 1 segundo y se va, pero bueno, imaginemos que es algo más complejo (si queréis conexiones, visitad uno de mis proyectos en Github), el hecho es que desde main() le hemos dicho que como función para el log utilice s.escribe, ¿cómo? Tenemos que decirle que el método es Salida::escribe y que está vinculado a s, pero lo hacemos todo del tirón con bind().
El tema de los placeholders, lo utilizaremos para especificar que ahí hay un argumento, y ahora mismo jugaremos con ellos.
En principio he querido incluir los namespaces (espacios de nombres) para que sepamos dónde está todo, aunque escribir std::placeholders::_1 da una pereza enorme, si tenemos cinco argumentos y autocompletamos ya no da pereza, sino que duelen los ojos, podemos hacer lo siguiente (ya quito el std:: también, porque tengo el using namespace std arriba):

1
2
3
4
5
6
7
8
int main()
{
  Salida s("default", 5);

  using namespace placeholders;
  conecta("totaki.com", bind(&Salida::escribe, &s, _1, _2));
  return 0;
}

O, si lo preferimos (con o sin std):

1
2
3
4
5
6
7
8
int main()
{
  Salida s("default", 5);

  namespace ph = std::placeholders;
  conecta("totaki.com", bind(&Salida::escribe, &s, ph::_1, ph::_2));
  return 0;
}

Orden de los argumentos

Cuando estamos programando, puede que estemos tomando prestado código de otras personas, imaginemos que la función conecta() es de un autor y la función para hacer el log lo ha escrito otra persona. El problema, puede ser que el logger necesite primero el nivel y luego el mensaje, pero la función de conecta llamará al logger y le pasará primero el mensaje y luego el nivel… y no es plan de ponernos a reescribir clases y métodos de terceros, porque puede ser muy engorroso.

Muy fácil, aquí entran los placeholders de antes, con los que íbamos a jugar nosotros (pego el ejemplo completo):

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
#include <iostream>
#include <string>
#include <functional>
#include <ctime>
#include <chrono>
#include <thread>

using namespace std;

struct Salida
{
  Salida(string nombre, int nivel):nombre(nombre), nivel(nivel)
  {
  }

  void escribe(int nivel, string mensaje)
  {
    if (nivel<this->nivel)
      return;

    string s(128, '\0');

    /* Vale, tenemos std::put_time() en C++11, pero yo uso GCC y hasta la
     versión 5.2 no está implementado, por lo que tengo que seguir usando
     las funciones de C. */

    std::time_t now = time(NULL);
    std::tm tm;
    localtime_r( &now, &tm );
    strftime( &s[0], s.size(), "%d/%m/%Y %h:%M:%S", &tm );
    cout << "[" << s << "] "<<nombre<<" Nivel: "<<nivel<<" | "<<mensaje<<endl;
  }
private:
  string nombre;
  int nivel;
};

void conecta(string servidor, function<void(string, int)> logger)
{
  logger("Conectando al servidor "+servidor+"...", 5);

  /* Hacemos como que conectamos */
  this_thread::sleep_for(chrono::seconds(1));

  logger("Servidor "+servidor+" conectado!", 6);
}

int main()
{
  Salida s("default", 5);

  namespace ph = std::placeholders;
  conecta("totaki.com", bind(&Salida::escribe, &s, ph::_2, ph::_1));
  return 0;
}

Si os fijáis, conecta() tiene primero el string y luego el int, y más tarde las llamadas las hace con ese orden. Entonces, desde main(), pondré primero el placeholder _2 y luego el _1, y problema resuelto, cuando haga la llamada podré invertir el orden de los argumentos.

Valores predefinidos

Ahora bien, mi función conecta() no soporta el parámetro nivel y no quiero quitarlo del logger, pues bien, puedo decir que el nivel para esa función siempre sea 5, por ejemplo (pego el código de conecta() y el main():

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void conecta(string servidor, function<void(string)> logger)
{
  logger("Conectando al servidor "+servidor+"...");

  /* Hacemos como que conectamos */
  this_thread::sleep_for(chrono::seconds(1));

  logger("Servidor "+servidor+" conectado!");
}

int main()
{
  Salida s("default", 5);

  namespace ph = std::placeholders;
  conecta("totaki.com", bind(&Salida::escribe, &s, 5, ph::_1));
  return 0;
}

En este caso, el logger de conecta sólo admite un argumento de tipo string, pues en main(), en la posición donde debe ir el nivel, pongo un 5 directamente (puedo poner una variable perfectamente), o cualquier cosa que desemboque en un int (que es el tipo del nivel).

Hasta la semana que viene!

Ya tenemos código para ir practicando, aunque me he ido dejando algunas cosillas en el tintero que tendré listas para la semana que viene, como argumentos por referencia, templates y algo más sobre lambdas. ¡Aún quedan muchos ejemplos por compilar!

Próximo post de la serie, el 23 de noviembre, pero mientras tanto iré publicando otras cosas.

Foto: Boris Lechaftols (Flickr CC)

También podría interesarte....

Leave a Reply