Publi

Callbacks en C++11 nuevas posibilidades para un software más potente (I)

My beautiful picture

My beautiful picture

Hace tiempo, hice una serie de posts sobre callbacks:

Hay algunos posts más, pero se salen del tema (y seguro que salen sugeridos más abajo). El caso es que dejé un poco el tema de lado y me gustaría retomarlo con los cambios de la especificación C++11 (vale, tenemos C++14 lista, pero la versión de 2011 es una de las que más cambios introdujeron (y que también valen para C++14). Aunque se ha escrito mucho sobre el tema, pero desde aquí quiero dar a conocer mi humilde visión.

Serán una serie de posts ya que es un tema muy amplio y me gusta poner gran cantidad de ejemplos.

Una variable que contiene una función

El ejemplo más sencillo, crear una variable que contenga una función, es decir, declaramos una variable y le asignamos como valor una función, así llamamos a la función que tenga asignada la variable en cada momento (podrá cambiar, podremos darle la opción al usuario para que tenga un valor u otro, o nos permitirá abstraer un poco más nuestro código):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <iostream>

using namespace std;

void hello()
{
  cout << "Hello world!"<<endl;
}

void goodbye()
{
  cout << "Goodbye world"<<endl;
}

int main()
{
  auto fun= hello;

  fun();
  fun = goodbye;
  fun();
  return 0;
}

Si usas g++, deberás hacer lo siguiente:

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

El programa, como vemos, asigna a la variable fun (de tipo auto, ya el compilador se encarga de poner el tipo que corresponda, lo cual nos ayuda mucho, que los tipos en C o C++ pueden ser muy complicados de sacar o largos de escribir, en este caso sería un tipo void (fun*)(), cuando tengamos argumentos, ya será otro cantar. Aunque, para hacerlo al más puro estilo C++11, deberíamos declararla como:

1
std::function<void()> fun=hello;

(como tenemos using namespace std no volveremos a decir que está ahí, pero es bueno saberlo) y hacer, arriba del todo, un

1
#include <functional>

Pero vamos, si vamos a hacer una inicialización inmediata como en el ejemplo, podemos utilizar auto y listo, es más, el archivo generado puede variar, usar function aunque nos ayuda bastante, se queda todo mucho más limpio y después podremos hacer más cosas, no deja de incluir algo de complejidad a nuestro programa.

Luego, como la función goodbye() y la función hello() son iguales, las dos son funciones que no devuelven nada y no tienen argumentos, a fun, le podemos asignar tanto hello, como goodbye, y llamando a fun se ejecutará la función pertinente.

De la misma manera, también funcionaría si hacemos llamadas a funciones estáticas dentro de una clase o un struct:

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
#include <iostream>
#include <string>

using namespace std;

struct MyClass
{
  static int hello(string what)
  {
    cout << "Hello "<<what<<"!"<<endl;
    return what.length();
  }

  static int goodbye(string what)
  {
    cout << "Goodbye "<<what<<"!"<<endl;
    return what.length();
  }
};

int main()
{
  auto fun= MyClass::hello;
  int n;

  n=fun("world");
  fun = MyClass::goodbye;
  n+=fun("boredom");

  cout << "Total: "<<n<<endl;
  return 0;
}

En este caso, he metido un argumento en cada método y un valor de salida, para que veamos que no hay diferencia en cuanto a la forma de usarlo, bueno, cuando llamamos a fun, tenemos que pasarle el argumento y éste nos dará un valor de salida, pero bueno, con auto, no tenemos que preocuparnos.
La forma de declarar la función en C (pero que no vamos a hacerlo, es sólo por curiosidad) sería:

1
int (*fun)(string)= MyClass::hello;

Y, en C++11, utilizando functional lo haríamos de esta forma:

1
function<int(string)> fun= MyClass::hello;

Debemos hacerlo así cuando no podamos utilizar auto, es decir, cuando no vayamos a inicializar la variable con ninguna función (y por tanto, el compilador no tenga forma de saber qué tipo queremos darle).

Usando funciones anónimas o lambdas

Podemos incluir la función en línea, es decir, dentro de la propia llamada a la función, es posible implementar una función que no podremos llamar de ninguna manera si no es con la variable a la que la asignamos o con la función a la que llamamos. (Ver ejemplo). Esto es muy útil cuando se trate, por ejemplo de funciones de búsqueda que nos piden una función para comparar o cuando a través de una función producimos una salida (por pantalla o por cualquier otro medio), o cuando debemos pasar a una función la forma con la que queremos tratar los datos:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <iostream>
#include <string>
#include <functional>

using namespace std;

void itera (int desde, int hasta, function<string(int)>llamada)
{
  for (int i=desde; i< hasta; ++i)
    cout << llamada(i) << endl;
}

int main()
{
  itera(1, 10, [] (int n)
    {
      return "El cuadrado de "+std::to_string(n)+" es "+std::to_string(n*n);
    });
  return 0;
}

La salida de esto será:

El cuadrado de 1 es 1
El cuadrado de 2 es 4
El cuadrado de 3 es 9
El cuadrado de 4 es 16
El cuadrado de 5 es 25
El cuadrado de 6 es 36
El cuadrado de 7 es 49
El cuadrado de 8 es 64
El cuadrado de 9 es 81

En este caso, la función itera hace un bucle empezando en desde y terminando en hasta-1 y en cada iteración ejecutará la función llamada, dicha función, se la hemos pasado en main() a través de una función anónima. Como vemos empezará en [](tipo valor,…) con tantos argumentos como tengamos. La salida, en este caso, la averiguará el compilador automáticamente, aunque si queremos imponer un tipo de salida, lo podremos hacer así:

1
2
3
4
  itera(1, 10, [] (int n) -> string
    {
      return "El cuadrado de "+std::to_string(n)+" es "+std::to_string(n*n);
    });

Otro ejemplo, utilizando la biblioteca algorithm (std), ordenando los valores de un vector, utilizando para ordenar una función anónima (para que podamos elegir el criterio de ordenación que queramos):

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
#include <iostream>
#include <vector>
#include <algorithm>

using namespace std;

void verVector(vector<int> v)
{
  cout << "Vector v: ";
  for (auto i : v)
    {
      cout << i << "\t";
    }
  cout << endl;
}

int main()
{
  vector<int> v = { 9, 4, -5, -10, 15, 1, -4, 5};

  verVector(v);

  cout << "Ordenación 1"<<endl;
  sort(v.begin(), v.end(), [] (int x, int y)
       {
     return ( (x*x)<(y*y) ) ;
       });
  verVector(v);

  cout << "Ordenación 2"<<endl;
  sort(v.begin(), v.end(), [] (int x, int y)
       {
     return ( (x*x)>(y*y) ) ;
       });
  verVector(v);

  return 0;
}

Las funciones anónimas tienen más cosas que trataremos más adelante, junto con muchos más ejemplos sobre callbacks en C++11 y posibilidades para nuestros programas. Por el momento, lo dejamos aquí hasta la próxima entrega (el día 9 de noviembre de 2015), en el que trataremos el tema de las llamadas a métodos de una clase. Esta vez, no serán métodos estáticos, sino que estarán asociados a un objeto.

Foto: Aaron (Flickr-cc)

También podría interesarte...

Leave a Reply