Publi

Callbacks en C++11, ejemplos con argumentos por referencia y templates (III)

7544361624_5fb63e7250_k

Volvemos a dar guerra con el tema que empezamos hace unas semanas, si no has seguido esta serie de posts te recomiendo echar un ojo a Callbacks en C++11 nuevas posibilidades para un software más potente y Callbacks en C++11, llamando a métodos con un objeto asociado. En ellos pongo muchos ejemplos de cómo asociar funciones o métodos a variables.

Argumentos por referencia

Es un tema que hemos dejado un poco de lado, pero sólo quiero hacer una pequeña puntualización. Y ya de paso, vemos que bind() no sólo vale para métodos con un objeto, también para funciones normales, lo único que no ponemos el objeto, y la clase sólo la pondremos si es un método estático.

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

using namespace std;

void contador(int &c)
{
  c++;
}

int main()
{
  int c = 0;
  auto cuenta = bind(&contador, placeholders::_1);

  for (auto i=0; i<100; ++i)
    cuenta(c);

  cout << "CONTADOR: "<<c<<endl;
  return 0;
}

En el ejemplo, vemos que contador() es una función que admite un argumento de tipo entero por referencia. Cuando desde main() lo vinculamos a cuenta() le decimos que el parámetro vendrá dado como parámetro de cuenta(), y luego en el bucle, le decimos que utilice como parámetro c.

Si lo ejecutamos, funcionará bien, CONTADOR devolverá 100. Pero, ¿qué ocurrirá si en lugar de usar placeholders::_1, le pasamos c directamente?

1
2
3
4
5
6
7
8
9
10
11
int main()
{
  int c = 0;
  auto cuenta = bind(&contador, c);

  for (auto i=0; i<100; ++i)
    cuenta();

  cout << "CONTADOR: "<<c<<endl;
  return 0;
}

Aquí contador() valdrá 0, es más, si en lugar de bind(&contador, c) ponemos bind(&contador, 5) compilará y ejecutará bien, y eso que un número no se puede pasar por referencia… y es que bind() lo pasa todo por valor, bueno, si queremos pasar algo por referencia, debemos indicarlo:

1
  auto cuenta = bind(&contador, ref(c));

Recibes una función, llámala como quieras

Bueno, tanto no, el objetivo es pasar un parámetro, que será una función, y llamarla, sin preocuparnos demasiado por el prototipo de la función. Es una cosa que siempre tenemos que tener presente, pero nos podemos olvidar de escribir demasiado.

Esto vale también para revisiones anteriores de C++, porque lo que hacemos es aprovechar la potencia de los templates para que sea el compilador el que se preocupe de que todo encaja:

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

using namespace std;

template<class Funcion>
void llamador(Funcion f)
{
  f(1, 2);
}

string mifun(int x, int y)
{
  cout << "HOLA MUNDO!"<<endl;
  return "ADIOS";
}

int main()
{
  llamador(mifun);
  return 0;
}

Aplicación práctica

Como aplicación, vamos a ver cómo recorremos vectores (parecido a std::for_each de algorithm), pero esta vez con una función que aplicará una condición, es decir, se recorre el vector, a cada elemento le preguntamos por una condición y si se cumple, llamamos a la función de acción:

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

using namespace std;

template<class Type, class Comparator, class Value, class Action>
void foreach_if(vector<Type> vect, Comparator comp, Value val, Action action)
{
  for (auto x : vect)
    {
      if (comp(x) == val)
    action(x);
    }
}

bool letras_pares(string x)
{
  return (x.length()%2==0);
}

int main()
{
  vector <string> myVector = {"Un tigre", "Dos tigres", "Tres tigres", "Cuatro tigres", "Veintidos tigres"};
  vector <int> myVectorDos = { 120, 50, 75, 12, 32, 45, 10, 60 };

  foreach_if (myVector, letras_pares, true, [](string v) {
      cout << "Valor del vector: "<<v<<endl;
    });

  cout << endl<<endl<<"Ahora con vector numerico " <<endl<<endl;

  foreach_if (myVectorDos,
          [] (int n) {  /* Función comparadora */
        return (n/10)%3;
          },
          1,        /* Resultado de la comparación*/
          [] (int v) {  /* Función acción*/
        cout << "Valor numerico: "<<v<<endl;
          });
  return 0;
}

Este ejemplo, si no fuera por los autos y las funciones anónimas o lambdas, podría haberse compilado perfectamente en versiones anteriores a C++11, pero con las nuevas características podemos hacer que todo tenga más sentido, puesto que nos da mucha más flexibilidad a la hora de escribir el código, al menos da mucho menos pie a errores. Personalmente con el código de versiones anteriores, era raro el día que compilaba a la primera…

Bueno, he querido recorrer dos vectores para que veamos la potencia de los templates en estos ejemplos. Aunque cuando compilemos, se creará código por separado para cada uno de los casos que le presentemos, es decir, si usamos dos tipos de vectores se creará código para un tipo y para el otro, el archivo compilado no gozará de un método general como este que vale para los dos.

Es más, podemos jugar con los tipos, pero siempre que un vector sea de un tipo Type, a la función comparadora le puedas pasar una variable de ese Type, y devuelva algo tipo Value y a la acción también le pases el Type, no hay problema. Es decir, podemos jugar todo lo que queramos con los tipos, siempre y cuando encajen al final.

Si por ejemplo queremos un ejemplo con iteradores, más parecido a std::for_each:

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

using namespace std;

template<class Iterator, class Comparator, class Value, class Action>
void foreach_if(Iterator inicio, Iterator fin, Comparator comp, Value val, Action action)
{
  while (inicio!=fin)
    {
      if (comp(*inicio) == val)
    action(*inicio);

      inicio++;
    }
}

bool letras_pares(string x)
{
  return (x.length()%2==0);
}

int main()
{
  vector <string> myVector = {"Un tigre", "Dos tigres", "Tres tigres", "Cuatro tigres", "Veintidos tigres"};
  vector <int> myVectorDos = { 120, 50, 75, 12, 32, 45, 10, 60 };

  foreach_if (myVector.begin(), myVector.end(), letras_pares, true, [](string v) {
      cout << "Valor del vector: "<<v<<endl;
    });

  cout << endl<<endl<<"Ahora con vector numerico " <<endl<<endl;

  foreach_if (myVectorDos.begin(), myVectorDos.end(),
          [] (int n) {  /* Función comparadora */
        return (n/10)%3;
          },
          1,        /* Resultado de la comparación*/
          [] (int v) {  /* Función acción*/
        cout << "Valor numerico: "<<v<<endl;
          });
  return 0;
}

Un apunte sobre lambdas

lambda
No pretendo hacer una guía tan amplia como esta: Lambda Functions in C++11 – the Definitive Guide por Alex Allain; la cual recomiendo. Mi aportación al tema irá en forma de guía rápida, ante un gran problema: a veces debemos utilizar elementos que están fuera de la función (variables, métodos, etc). Por lo que debemos capturarlos y pasárselos (y a veces no podemos hacerlo como si incluyéramos argumentos, o sería muy difícil y raro):

Capturar variables por referencia

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

using namespace std;

template <class Funcion>
void llamador(Funcion f)
{
  f();
}

int main()
{
  int a = 10;
  llamador([&a]() {
      a++;
    });
  cout << a << std::endl;
  return 0;
}

Por ejemplo, queremos pasar la variable a por referencia, para poder incrementarla dentro de nuestra función anónima. Lo podemos hacer tal y como indica el ejemplo. Es más, si quisiéramos utilizar todas las variables a nuestro alcance por referencia, también podríamos, si ponemos simplemente [&]. Sería el compilador el que vería qué utilizamos y qué no utilizamos para capturarlas. En este caso, si quisiéramos capturarlas por valor podemos escribir [=] o el nombre de la variable.

Capturar variables por referencia y por valor

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

using namespace std;

template <class Funcion>
void llamador(Funcion f)
{
  f();
}

int main()
{
  int a = 10;
  int incremento = 5;
  llamador([=, &a]() {
      a+=incremento;
    });
  cout << a << std::endl;

  return 0;
}

Según el ejemplo, estaremos capturando todas las variables por valor (creando una copia), y la variable a por referencia.

El puntero this

this puede ser un caso especial. Imaginémonos que tenemos un ejemplo de cliente y vendedor, al objeto cliente le asignamos una cantidad de dinero y el objeto vendedor le cobrará al cliente ese dinero. Pero el cliente, necesita de una función para poder pagar (imaginad que vamos a hacer algunas comprobaciones sobre el precio, o el cliente debe reservar un dinero para ahorrar…):

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

using namespace std;

class Cliente
{
public:
  Cliente(int money): dinero(money) {
    funcionpagar = [this] (int cantidad) {
      dinero-=cantidad;
      return true;
    };
  }
  bool paga(int cantidad)
  {
    if (dinero>=cantidad)
      return realiza_pago(cantidad, funcionpagar);
    else
      return false;
  }
private:
  bool realiza_pago(int cantidad, function<bool(int)> algo)
  {
    algo(cantidad);
  }

  function<bool(int)> funcionpagar;
  int dinero;
};

class Vendedor
{
public:
  Vendedor() { }
  ~Vendedor() { }
  void vende(int dinero, Cliente c)
  {
    if (c.paga(dinero))
      cout << "El cliente puede pagar"<<endl;
    else
      cout << "El cliente NO TIENE DINERO"<<endl;
  }
};

int main()
{
  Cliente c(100);
  Vendedor v;

  v.vende(100, c);
  return 0;
}

Fijáos cómo defino en el constructor de Cliente la función que usará el método realiza_pago(), algo así como para definir su comportamiento (aunque al final estamos haciendo cosas muy simples, esto puede dar pie a complicarlo sobremanera.

Cuando definimos nuestro lambda, le pasamos [this], al ser un puntero especial, identificará cuando llamemos a métodos o atributos de la clase para asignarles el ámbito correspondiente… y por supuesto podemos utilizar expresiones tipo «this->»

¿Has utilizado este tipo de elementos en C++? ¿Qué te parecen? No dudes en dejar un comentario ante cualquier duda o puntualización.

Foto: Dimitri Kallnin (Flickr-CC)
Foto: Mason Jones (Unsplash)

También podría interesarte....

There are 3 comments left Ir a comentario

  1. Pingback: Callbacks en C++11, ejemplos con argumentos por referencia y templates (III) | PlanetaLibre /

  2. yitzchak kerrigan /
    Usando Google Chrome Google Chrome 120.0.0.0 en Windows Windows NT

    No More Worries: 구글환불 takes away your worries! When eligible, request a refund effortlessly through Google. It’s designed for simplicity, ensuring a stress-free experience.

  3. Alice Bobby /
    Usando Google Chrome Google Chrome 121.0.0.0 en Windows Windows NT

    Because of its straightforward design, using it should not cause any stress. tunnel rush

Leave a Reply