Funciones de devolución de llamada en C ++

303

En C ++, ¿cuándo y cómo utiliza una función de devolución de llamada?

EDITAR:
Me gustaría ver un ejemplo simple para escribir una función de devolución de llamada.

cpx
fuente
[Esto] ( thispointer.com/… ) explica los conceptos básicos sobre las funciones de devolución de llamada muy bien y fácil de entender el concepto.
Anurag Singh

Respuestas:

449

Nota: La mayoría de las respuestas cubren punteros de función, que es una posibilidad para lograr la lógica de "devolución de llamada" en C ++, pero a partir de hoy no creo que sea la más favorable.

¿Qué son las devoluciones de llamada (?) Y por qué usarlas (!)

Una devolución de llamada es invocable (ver más abajo) aceptada por una clase o función, utilizada para personalizar la lógica actual dependiendo de esa devolución de llamada.

Una razón para usar devoluciones de llamada es escribir genéricos código que es independiente de la lógica en la función llamada y puede reutilizarse con diferentes devoluciones de llamada.

Muchas funciones de la biblioteca de algoritmos estándar <algorithm>utilizan devoluciones de llamada. Por ejemplo, el for_eachalgoritmo aplica una devolución de llamada unaria a cada elemento en un rango de iteradores:

template<class InputIt, class UnaryFunction>
UnaryFunction for_each(InputIt first, InputIt last, UnaryFunction f)
{
  for (; first != last; ++first) {
    f(*first);
  }
  return f;
}

que se puede utilizar para incrementar primero y luego imprimir un vector al pasar callables apropiados, por ejemplo:

std::vector<double> v{ 1.0, 2.2, 4.0, 5.5, 7.2 };
double r = 4.0;
std::for_each(v.begin(), v.end(), [&](double & v) { v += r; });
std::for_each(v.begin(), v.end(), [](double v) { std::cout << v << " "; });

que imprime

5 6.2 8 9.5 11.2

Otra aplicación de las devoluciones de llamada es la notificación a las personas que llaman de ciertos eventos que permite una cierta flexibilidad de tiempo de compilación / estática.

Personalmente, uso una biblioteca de optimización local que usa dos devoluciones de llamada diferentes:

  • Se llama a la primera devolución de llamada si se requiere un valor de función y el gradiente basado en un vector de valores de entrada (devolución de llamada lógica: determinación del valor de función / derivación de gradiente).
  • La segunda devolución de llamada se llama una vez para cada paso del algoritmo y recibe cierta información sobre la convergencia del algoritmo (devolución de llamada de notificación).

Por lo tanto, el diseñador de la biblioteca no está a cargo de decidir qué sucede con la información que se le da al programador a través de la devolución de llamada de notificación y no necesita preocuparse sobre cómo determinar realmente los valores de función porque son proporcionados por la devolución de llamada lógica. Hacer las cosas bien es una tarea que debe realizar el usuario de la biblioteca y mantiene la biblioteca delgada y más genérica.

Además, las devoluciones de llamada pueden permitir un comportamiento dinámico en tiempo de ejecución.

Imagine algún tipo de clase de motor de juego que tenga una función que se active, cada vez que los usuarios presionen un botón en su teclado y un conjunto de funciones que controlan el comportamiento de su juego. Con las devoluciones de llamada puede (re) decidir en tiempo de ejecución qué acción se tomará.

void player_jump();
void player_crouch();

class game_core
{
    std::array<void(*)(), total_num_keys> actions;
    // 
    void key_pressed(unsigned key_id)
    {
        if(actions[key_id]) actions[key_id]();
    }
    // update keybind from menu
    void update_keybind(unsigned key_id, void(*new_action)())
    {
        actions[key_id] = new_action;
    }
};

Aquí la función key_pressedutiliza las devoluciones de llamada almacenadas actionspara obtener el comportamiento deseado cuando se presiona una tecla determinada. Si el jugador elige cambiar el botón para saltar, el motor puede llamar

game_core_instance.update_keybind(newly_selected_key, &player_jump);

y, por lo tanto, cambia el comportamiento de una llamada a key_pressed(a la que se llama player_jump) una vez que se presiona este botón la próxima vez que se juega.

¿Qué son las llamadas en C ++ (11)?

Consulte los conceptos de C ++: invocables en cppreference para obtener una descripción más formal.

La funcionalidad de devolución de llamada se puede realizar de varias maneras en C ++ (11) ya que se pueden llamar varias cosas diferentes * :

  • Punteros de función (incluidos punteros a funciones miembro)
  • std::function objetos
  • Expresiones lambda
  • Enlazar expresiones
  • Objetos de función (clases con operador de llamada de función sobrecargado operator())

* Nota: el puntero a los miembros de datos también se puede llamar pero no se llama a ninguna función.

Varias formas importantes de escribir devoluciones de llamada en detalle

  • X.1 "Escribir" una devolución de llamada en esta publicación significa la sintaxis para declarar y nombrar el tipo de devolución de llamada.
  • X.2 "Llamar" una devolución de llamada se refiere a la sintaxis para llamar a esos objetos.
  • X.3 "Usar" una devolución de llamada significa la sintaxis al pasar argumentos a una función que utiliza una devolución de llamada.

Nota: A partir de C ++ 17, f(...)se puede escribir una llamada como std::invoke(f, ...)que también maneja el puntero al caso miembro.

1. Punteros de función

Un puntero de función es el tipo 'más simple' (en términos de generalidad; en términos de legibilidad, posiblemente el peor) que puede tener una devolución de llamada.

Tengamos una función simple foo:

int foo (int x) { return 2+x; }

1.1 Escribir un puntero de función / notación de tipo

Un tipo de puntero de función tiene la notación

return_type (*)(parameter_type_1, parameter_type_2, parameter_type_3)
// i.e. a pointer to foo has the type:
int (*)(int)

donde se verá un tipo de puntero de función con nombre

return_type (* name) (parameter_type_1, parameter_type_2, parameter_type_3)

// i.e. f_int_t is a type: function pointer taking one int argument, returning int
typedef int (*f_int_t) (int); 

// foo_p is a pointer to function taking int returning int
// initialized by pointer to function foo taking int returning int
int (* foo_p)(int) = &foo; 
// can alternatively be written as 
f_int_t foo_p = &foo;

La usingdeclaración nos da la opción de hacer las cosas un poco más legibles, ya que typedeffor f_int_ttambién se puede escribir como:

using f_int_t = int(*)(int);

Donde (al menos para mí) es más claro cuál es f_int_tel nuevo alias de tipo y el reconocimiento del tipo de puntero de función también es más fácil

Y una declaración de una función que utiliza una devolución de llamada del tipo de puntero de función será:

// foobar having a callback argument named moo of type 
// pointer to function returning int taking int as its argument
int foobar (int x, int (*moo)(int));
// if f_int is the function pointer typedef from above we can also write foobar as:
int foobar (int x, f_int_t moo);

1.2 Notación de llamada de devolución de llamada

La notación de llamada sigue la sintaxis de llamada de función simple:

int foobar (int x, int (*moo)(int))
{
    return x + moo(x); // function pointer moo called using argument x
}
// analog
int foobar (int x, f_int_t moo)
{
    return x + moo(x); // function pointer moo called using argument x
}

1.3 Notación de uso de devolución de llamada y tipos compatibles

Una función de devolución de llamada que toma un puntero de función se puede llamar utilizando punteros de función.

Usar una función que toma una devolución de llamada de puntero de función es bastante simple:

 int a = 5;
 int b = foobar(a, foo); // call foobar with pointer to foo as callback
 // can also be
 int b = foobar(a, &foo); // call foobar with pointer to foo as callback

1.4 Ejemplo

Se puede escribir una función que no dependa de cómo funciona la devolución de llamada:

void tranform_every_int(int * v, unsigned n, int (*fp)(int))
{
  for (unsigned i = 0; i < n; ++i)
  {
    v[i] = fp(v[i]);
  }
}

donde podrían ser posibles devoluciones de llamada

int double_int(int x) { return 2*x; }
int square_int(int x) { return x*x; }

utilizado como

int a[5] = {1, 2, 3, 4, 5};
tranform_every_int(&a[0], 5, double_int);
// now a == {2, 4, 6, 8, 10};
tranform_every_int(&a[0], 5, square_int);
// now a == {4, 16, 36, 64, 100};

2. Puntero a la función miembro

Un puntero a la función miembro (de alguna clase C) es un tipo especial de puntero de función (e incluso más complejo) que requiere un objeto de tipo Cpara operar.

struct C
{
    int y;
    int foo(int x) const { return x+y; }
};

2.1 Escribir puntero a la función miembro / notación de tipo

Un puntero al tipo de función miembro para alguna clase Ttiene la notación

// can have more or less parameters
return_type (T::*)(parameter_type_1, parameter_type_2, parameter_type_3)
// i.e. a pointer to C::foo has the type
int (C::*) (int)

donde un puntero con nombre a la función miembro , en analogía al puntero de la función, se verá así:

return_type (T::* name) (parameter_type_1, parameter_type_2, parameter_type_3)

// i.e. a type `f_C_int` representing a pointer to member function of `C`
// taking int returning int is:
typedef int (C::* f_C_int_t) (int x); 

// The type of C_foo_p is a pointer to member function of C taking int returning int
// Its value is initialized by a pointer to foo of C
int (C::* C_foo_p)(int) = &C::foo;
// which can also be written using the typedef:
f_C_int_t C_foo_p = &C::foo;

Ejemplo: declarar una función tomando un puntero a la devolución de llamada de la función miembro como uno de sus argumentos:

// C_foobar having an argument named moo of type pointer to member function of C
// where the callback returns int taking int as its argument
// also needs an object of type c
int C_foobar (int x, C const &c, int (C::*moo)(int));
// can equivalently declared using the typedef above:
int C_foobar (int x, C const &c, f_C_int_t moo);

2.2 Notación de devolución de llamada

Se Cpuede invocar la función de puntero a miembro de , con respecto a un objeto de tipo Cmediante operaciones de acceso de miembro en el puntero desreferenciado. Nota: ¡Se requiere paréntesis!

int C_foobar (int x, C const &c, int (C::*moo)(int))
{
    return x + (c.*moo)(x); // function pointer moo called for object c using argument x
}
// analog
int C_foobar (int x, C const &c, f_C_int_t moo)
{
    return x + (c.*moo)(x); // function pointer moo called for object c using argument x
}

Nota: Si Chay disponible un puntero a, la sintaxis es equivalente (donde también se Cdebe desreferenciar el puntero a ):

int C_foobar_2 (int x, C const * c, int (C::*meow)(int))
{
    if (!c) return x;
    // function pointer meow called for object *c using argument x
    return x + ((*c).*meow)(x); 
}
// or equivalent:
int C_foobar_2 (int x, C const * c, int (C::*meow)(int))
{
    if (!c) return x;
    // function pointer meow called for object *c using argument x
    return x + (c->*meow)(x); 
}

2.3 Notación de uso de devolución de llamada y tipos compatibles

Una función de devolución de llamada que toma un puntero de función miembro de clase Tse puede llamar usando un puntero de clase miembro de clase T.

El uso de una función que toma un puntero a la devolución de llamada de la función miembro es, en analogía a los punteros de función, también bastante simple:

 C my_c{2}; // aggregate initialization
 int a = 5;
 int b = C_foobar(a, my_c, &C::foo); // call C_foobar with pointer to foo as its callback

3. std::functionobjetos (encabezado<functional> )

La std::functionclase es un contenedor de funciones polimórficas para almacenar, copiar o invocar invocables.

3.1 Escribir una std::functionnotación de objeto / tipo

El tipo de un std::functionobjeto que almacena un invocable se ve así:

std::function<return_type(parameter_type_1, parameter_type_2, parameter_type_3)>

// i.e. using the above function declaration of foo:
std::function<int(int)> stdf_foo = &foo;
// or C::foo:
std::function<int(const C&, int)> stdf_C_foo = &C::foo;

3.2 Notación de llamada de devolución de llamada

La clase std::functionha operator()definido cuál puede usarse para invocar su objetivo.

int stdf_foobar (int x, std::function<int(int)> moo)
{
    return x + moo(x); // std::function moo called
}
// or 
int stdf_C_foobar (int x, C const &c, std::function<int(C const &, int)> moo)
{
    return x + moo(c, x); // std::function moo called using c and x
}

3.3 Notación de uso de devolución de llamada y tipos compatibles

La std::functiondevolución de llamada es más genérica que los punteros de función o el puntero a la función miembro, ya que se pueden pasar diferentes tipos y convertirlos implícitamente en un std::functionobjeto.

3.3.1 Punteros de función y punteros a funciones miembro

Un puntero de función

int a = 2;
int b = stdf_foobar(a, &foo);
// b == 6 ( 2 + (2+2) )

o un puntero a la función miembro

int a = 2;
C my_c{7}; // aggregate initialization
int b = stdf_C_foobar(a, c, &C::foo);
// b == 11 == ( 2 + (7+2) )

puede ser usado.

3.3.2 Expresiones lambda

Un cierre sin nombre de una expresión lambda se puede almacenar en un std::functionobjeto:

int a = 2;
int c = 3;
int b = stdf_foobar(a, [c](int x) -> int { return 7+c*x; });
// b == 15 ==  a + (7*c*a) == 2 + (7+3*2)

3.3.3 std::bindexpresiones

Se std::bindpuede pasar el resultado de una expresión. Por ejemplo, vinculando parámetros a una llamada de puntero de función:

int foo_2 (int x, int y) { return 9*x + y; }
using std::placeholders::_1;

int a = 2;
int b = stdf_foobar(a, std::bind(foo_2, _1, 3));
// b == 23 == 2 + ( 9*2 + 3 )
int c = stdf_foobar(a, std::bind(foo_2, 5, _1));
// c == 49 == 2 + ( 9*5 + 2 )

Donde también los objetos pueden vincularse como el objeto para la invocación de las funciones de puntero a miembro:

int a = 2;
C const my_c{7}; // aggregate initialization
int b = stdf_foobar(a, std::bind(&C::foo, my_c, _1));
// b == 1 == 2 + ( 2 + 7 )

3.3.4 Objetos de función

Los objetos de clases que tienen una operator()sobrecarga adecuada también se pueden almacenar dentro de un std::functionobjeto.

struct Meow
{
  int y = 0;
  Meow(int y_) : y(y_) {}
  int operator()(int x) { return y * x; }
};
int a = 11;
int b = stdf_foobar(a, Meow{8});
// b == 99 == 11 + ( 8 * 11 )

3.4 Ejemplo

Cambiar el ejemplo del puntero de función para usar std::function

void stdf_tranform_every_int(int * v, unsigned n, std::function<int(int)> fp)
{
  for (unsigned i = 0; i < n; ++i)
  {
    v[i] = fp(v[i]);
  }
}

le da mucha más utilidad a esa función porque (ver 3.3) tenemos más posibilidades de usarla:

// using function pointer still possible
int a[5] = {1, 2, 3, 4, 5};
stdf_tranform_every_int(&a[0], 5, double_int);
// now a == {2, 4, 6, 8, 10};

// use it without having to write another function by using a lambda
stdf_tranform_every_int(&a[0], 5, [](int x) -> int { return x/2; });
// now a == {1, 2, 3, 4, 5}; again

// use std::bind :
int nine_x_and_y (int x, int y) { return 9*x + y; }
using std::placeholders::_1;
// calls nine_x_and_y for every int in a with y being 4 every time
stdf_tranform_every_int(&a[0], 5, std::bind(nine_x_and_y, _1, 4));
// now a == {13, 22, 31, 40, 49};

4. Tipo de devolución de llamada con plantilla

Usando plantillas, el código que llama a la devolución de llamada puede ser aún más general que usar std::functionobjetos.

Tenga en cuenta que las plantillas son una característica de tiempo de compilación y una herramienta de diseño para el polimorfismo en tiempo de compilación. Si el comportamiento dinámico del tiempo de ejecución se logra mediante devoluciones de llamada, las plantillas ayudarán pero no inducirán la dinámica del tiempo de ejecución.

4.1 Escritura (anotaciones de tipo) y llamadas callbacks con plantillas

La generalización, es decir, el std_ftransform_every_intcódigo de arriba aún más se puede lograr mediante el uso de plantillas:

template<class R, class T>
void stdf_transform_every_int_templ(int * v,
  unsigned const n, std::function<R(T)> fp)
{
  for (unsigned i = 0; i < n; ++i)
  {
    v[i] = fp(v[i]);
  }
}

con una sintaxis aún más general (así como la más fácil) para que un tipo de devolución de llamada sea un argumento con plantilla simple y deducible:

template<class F>
void transform_every_int_templ(int * v, 
  unsigned const n, F f)
{
  std::cout << "transform_every_int_templ<" 
    << type_name<F>() << ">\n";
  for (unsigned i = 0; i < n; ++i)
  {
    v[i] = f(v[i]);
  }
}

Nota: La salida incluida imprime el nombre del tipo deducido para el tipo de plantilla F. La implementación de type_namese da al final de esta publicación.

La implementación más general para la transformación unaria de un rango es parte de la biblioteca estándar, es decir std::transform, que también está diseñada con respecto a los tipos iterados.

template<class InputIt, class OutputIt, class UnaryOperation>
OutputIt transform(InputIt first1, InputIt last1, OutputIt d_first,
  UnaryOperation unary_op)
{
  while (first1 != last1) {
    *d_first++ = unary_op(*first1++);
  }
  return d_first;
}

4.2 Ejemplos que utilizan devoluciones de llamada con plantillas y tipos compatibles

Los tipos compatibles para el std::functionmétodo de devolución de llamada con plantilla stdf_transform_every_int_templson idénticos a los tipos mencionados anteriormente (ver 3.4).

Sin embargo, al usar la versión con plantilla, la firma de la devolución de llamada utilizada puede cambiar un poco:

// Let
int foo (int x) { return 2+x; }
int muh (int const &x) { return 3+x; }
int & woof (int &x) { x *= 4; return x; }

int a[5] = {1, 2, 3, 4, 5};
stdf_transform_every_int_templ<int,int>(&a[0], 5, &foo);
// a == {3, 4, 5, 6, 7}
stdf_transform_every_int_templ<int, int const &>(&a[0], 5, &muh);
// a == {6, 7, 8, 9, 10}
stdf_transform_every_int_templ<int, int &>(&a[0], 5, &woof);

Nota: std_ftransform_every_int(versión sin plantilla; ver arriba) funciona foopero no se usa muh.

// Let
void print_int(int * p, unsigned const n)
{
  bool f{ true };
  for (unsigned i = 0; i < n; ++i)
  {
    std::cout << (f ? "" : " ") << p[i]; 
    f = false;
  }
  std::cout << "\n";
}

El parámetro simple con plantilla de transform_every_int_templpuede ser cualquier tipo invocable posible.

int a[5] = { 1, 2, 3, 4, 5 };
print_int(a, 5);
transform_every_int_templ(&a[0], 5, foo);
print_int(a, 5);
transform_every_int_templ(&a[0], 5, muh);
print_int(a, 5);
transform_every_int_templ(&a[0], 5, woof);
print_int(a, 5);
transform_every_int_templ(&a[0], 5, [](int x) -> int { return x + x + x; });
print_int(a, 5);
transform_every_int_templ(&a[0], 5, Meow{ 4 });
print_int(a, 5);
using std::placeholders::_1;
transform_every_int_templ(&a[0], 5, std::bind(foo_2, _1, 3));
print_int(a, 5);
transform_every_int_templ(&a[0], 5, std::function<int(int)>{&foo});
print_int(a, 5);

Se imprime el código anterior:

1 2 3 4 5
transform_every_int_templ <int(*)(int)>
3 4 5 6 7
transform_every_int_templ <int(*)(int&)>
6 8 10 12 14
transform_every_int_templ <int& (*)(int&)>
9 11 13 15 17
transform_every_int_templ <main::{lambda(int)#1} >
27 33 39 45 51
transform_every_int_templ <Meow>
108 132 156 180 204
transform_every_int_templ <std::_Bind<int(*(std::_Placeholder<1>, int))(int, int)>>
975 1191 1407 1623 1839
transform_every_int_templ <std::function<int(int)>>
977 1193 1409 1625 1841

type_name implementación utilizada anteriormente

#include <type_traits>
#include <typeinfo>
#include <string>
#include <memory>
#include <cxxabi.h>

template <class T>
std::string type_name()
{
  typedef typename std::remove_reference<T>::type TR;
  std::unique_ptr<char, void(*)(void*)> own
    (abi::__cxa_demangle(typeid(TR).name(), nullptr,
    nullptr, nullptr), std::free);
  std::string r = own != nullptr?own.get():typeid(TR).name();
  if (std::is_const<TR>::value)
    r += " const";
  if (std::is_volatile<TR>::value)
    r += " volatile";
  if (std::is_lvalue_reference<T>::value)
    r += " &";
  else if (std::is_rvalue_reference<T>::value)
    r += " &&";
  return r;
}
Pixelchemist
fuente
35
@BogeyJammer: En caso de que no lo hayas notado: la respuesta tiene dos partes. 1. Una explicación general de "devoluciones de llamada" con un pequeño ejemplo. 2. Una lista completa de diferentes llamadas y las formas de escribir código usando devoluciones de llamada. Le invitamos a no profundizar en los detalles o leer la respuesta completa, pero solo porque no desea una vista detallada, no es el caso que la respuesta sea ineficaz o "brutalmente copiada". El tema es "devoluciones de llamada de c ++". Incluso si la parte 1 está bien para OP, otros pueden encontrar útil la parte 2. Siéntase libre de señalar cualquier falta de información o crítica constructiva para la primera parte en lugar de -1.
Pixelchemist
1
La parte 1 no es amigable para principiantes y es lo suficientemente clara. No puedo ser más constructivo al decir que no logró aprenderme algo. Y la parte 2, no fue solicitada, inundando la página y está fuera de discusión aunque pretendas que es útil a pesar del hecho de que comúnmente se encuentra en documentación dedicada donde se busca dicha información detallada en primer lugar. Definitivamente mantengo el voto negativo. Un voto único representa una opinión personal, así que acéptelo y respételo.
Bogey Jammer
24
@BogeyJammer No soy nuevo en programación pero soy nuevo en "c ++ moderno". Esta respuesta me da el contexto exacto que necesito para razonar sobre el papel que juegan las devoluciones de llamada, específicamente, c ++. El OP puede no haber pedido múltiples ejemplos, pero es habitual en SO, en una búsqueda interminable para educar a un mundo de tontos, para enumerar todas las posibles soluciones a una pregunta. Si se lee demasiado como un libro, el único consejo que puedo ofrecer es practicar un poco leyendo algunos de ellos .
dcow
int b = foobar(a, foo); // call foobar with pointer to foo as callback, este es un error tipográfico ¿verdad? foodebería ser un puntero para que esto funcione AFAIK.
konoufo
@konoufo: [conv.func]del estándar C ++ 11 dice: " Un valor de l del tipo de función T se puede convertir en un valor de tipo" puntero a T. " El resultado es un puntero a la función. "Esta es una conversión estándar y, como tal, sucede implícitamente. Uno podría (por supuesto) usar el puntero de función aquí.
Pixelchemist
160

También existe la forma C de hacer devoluciones de llamada: punteros de función

//Define a type for the callback signature,
//it is not necessary, but makes life easier

//Function pointer called CallbackType that takes a float
//and returns an int
typedef int (*CallbackType)(float);  


void DoWork(CallbackType callback)
{
  float variable = 0.0f;

  //Do calculations

  //Call the callback with the variable, and retrieve the
  //result
  int result = callback(variable);

  //Do something with the result
}

int SomeCallback(float variable)
{
  int result;

  //Interpret variable

  return result;
}

int main(int argc, char ** argv)
{
  //Pass in SomeCallback to the DoWork
  DoWork(&SomeCallback);
}

Ahora, si desea pasar métodos de clase como devoluciones de llamada, las declaraciones a esos punteros de función tienen declaraciones más complejas, por ejemplo:

//Declaration:
typedef int (ClassName::*CallbackType)(float);

//This method performs work using an object instance
void DoWorkObject(CallbackType callback)
{
  //Class instance to invoke it through
  ClassName objectInstance;

  //Invocation
  int result = (objectInstance.*callback)(1.0f);
}

//This method performs work using an object pointer
void DoWorkPointer(CallbackType callback)
{
  //Class pointer to invoke it through
  ClassName * pointerInstance;

  //Invocation
  int result = (pointerInstance->*callback)(1.0f);
}

int main(int argc, char ** argv)
{
  //Pass in SomeCallback to the DoWork
  DoWorkObject(&ClassName::Method);
  DoWorkPointer(&ClassName::Method);
}
Ramon Zarazua B.
fuente
1
Hay un error en el ejemplo del método de clase. La invocación debe ser: (instancia. * Devolución de llamada) (1.0f)
CarlJohnson
Gracias por señalar eso. Agregaré ambos para ilustrar la invocación a través de un objeto y a través de un puntero de objeto.
Ramon Zarazua B.
3
Esto tiene la desventaja de std :: tr1: función en que la devolución de llamada se escribe por clase; Esto hace que sea poco práctico usar devoluciones de llamada de estilo C cuando el objeto que realiza la llamada no conoce la clase del objeto a llamar.
bleater
¿Cómo podría hacerlo sin typedefel tipo de devolución de llamada? ¿Es posible?
Tomáš Zato - Restablece a Mónica el
1
Sí tu puedes. typedefes solo azúcar sintáctico para hacerlo más legible. Sin typedef, la definición de DoWorkObject para los punteros de función sería la siguiente: void DoWorkObject(int (*callback)(float)). Los consejos de los miembros serían:void DoWorkObject(int (ClassName::*callback)(float))
Ramon Zarazua B.
68

Scott Meyers da un buen ejemplo:

class GameCharacter;
int defaultHealthCalc(const GameCharacter& gc);

class GameCharacter
{
public:
  typedef std::function<int (const GameCharacter&)> HealthCalcFunc;

  explicit GameCharacter(HealthCalcFunc hcf = defaultHealthCalc)
  : healthFunc(hcf)
  { }

  int healthValue() const { return healthFunc(*this); }

private:
  HealthCalcFunc healthFunc;
};

Creo que el ejemplo lo dice todo.

std::function<> es la forma "moderna" de escribir devoluciones de llamada de C ++.

Karl von Moor
fuente
1
Por interés, ¿en qué libro da SM este ejemplo? Saludos :)
Sam-w
55
Sé que esto es antiguo, pero debido a que casi comencé a hacer esto y terminó sin funcionar en mi configuración (mingw), si está utilizando la versión GCC <4.x, este método no es compatible. Algunas de las dependencias que estoy usando no se compilarán sin mucho trabajo en la versión gcc> = 4.0.1, por lo que estoy atascado con el uso de buenas devoluciones de llamadas estilo C, que funcionan bien.
OzBarry
38

Una función de devolución de llamada es un método que se pasa a una rutina y se llama en algún momento por la rutina a la que se pasa.

Esto es muy útil para hacer software reutilizable. Por ejemplo, muchas API del sistema operativo (como la API de Windows) utilizan muchas devoluciones de llamada.

Por ejemplo, si desea trabajar con archivos en una carpeta, puede llamar a una función API, con su propia rutina, y su rutina se ejecuta una vez por archivo en la carpeta especificada. Esto permite que la API sea muy flexible.

Reed Copsey
fuente
63
Esta respuesta realmente no le dice al programador promedio algo que no sabía. Estoy aprendiendo C ++ mientras estoy familiarizado con muchos otros lenguajes. La devolución de llamada en general no me concierne.
Tomáš Zato - Restablece a Mónica el
17

La respuesta aceptada es muy útil y bastante completa. Sin embargo, el OP establece

Me gustaría ver un ejemplo simple para escribir una función de devolución de llamada.

Así que aquí tienes, desde C ++ 11 que tienes, std::functionasí que no hay necesidad de punteros de función y cosas similares:

#include <functional>
#include <string>
#include <iostream>

void print_hashes(std::function<int (const std::string&)> hash_calculator) {
    std::string strings_to_hash[] = {"you", "saved", "my", "day"};
    for(auto s : strings_to_hash)
        std::cout << s << ":" << hash_calculator(s) << std::endl;    
}

int main() {
    print_hashes( [](const std::string& str) {   /** lambda expression */
        int result = 0;
        for (int i = 0; i < str.length(); i++)
            result += pow(31, i) * str.at(i);
        return result;
    });
    return 0;
}

Por cierto, este ejemplo es real, porque desea llamar a la función print_hashescon diferentes implementaciones de funciones hash, para este propósito proporcioné una simple. Recibe una cadena, devuelve un int (un valor hash de la cadena proporcionada), y todo lo que necesita recordar de la parte de sintaxis es std::function<int (const std::string&)>que describe dicha función como un argumento de entrada de la función que la invocará.

Miljen Mikic
fuente
De todas las respuestas anteriores, esta me hizo comprender qué son las devoluciones de llamada y cómo usarlas. Gracias.
Mehar Charan Sahai
@MeharCharanSahai Me alegra escucharlo :) De nada.
Miljen Mikic
9

No hay un concepto explícito de una función de devolución de llamada en C ++. Los mecanismos de devolución de llamada a menudo se implementan mediante punteros de función, objetos de función u objetos de devolución de llamada. Los programadores tienen que diseñar e implementar explícitamente la funcionalidad de devolución de llamada.

Editar basado en comentarios:

A pesar de los comentarios negativos que ha recibido esta respuesta, no está mal. Trataré de explicar mejor de dónde vengo.

C y C ++ tienen todo lo que necesita para implementar funciones de devolución de llamada. La forma más común y trivial de implementar una función de devolución de llamada es pasar un puntero de función como argumento de función.

Sin embargo, las funciones de devolución de llamada y los punteros de función no son sinónimos. Un puntero de función es un mecanismo de lenguaje, mientras que una función de devolución de llamada es un concepto semántico. Los punteros de función no son la única forma de implementar una función de devolución de llamada: también puede usar functores e incluso funciones virtuales de variedad de jardín. Lo que hace que una función llame a una devolución de llamada no es el mecanismo utilizado para identificar y llamar a la función, sino el contexto y la semántica de la llamada. Decir que algo es una función de devolución de llamada implica una separación mayor que la normal entre la función de llamada y la función específica que se llama, un acoplamiento conceptual más flexible entre la persona que llama y la persona que llama, con la persona que llama tiene un control explícito sobre lo que se llama.

Por ejemplo, la documentación de .NET para IFormatProvider dice que "GetFormat es un método de devolución de llamada" , a pesar de que es solo un método de interfaz común y corriente . No creo que nadie argumente que todas las llamadas a métodos virtuales son funciones de devolución de llamada. Lo que hace que GetFormat sea un método de devolución de llamada no es la mecánica de cómo se pasa o se invoca, sino la semántica de la persona que llama que elige el método GetFormat de objeto.

Algunos idiomas incluyen características con semántica de devolución de llamada explícita, generalmente relacionadas con eventos y manejo de eventos. Por ejemplo, C # tiene el tipo de evento con sintaxis y semántica diseñada explícitamente en torno al concepto de devoluciones de llamada. Visual Basic tiene su cláusula Handles , que declara explícitamente que un método es una función de devolución de llamada mientras abstrae el concepto de delegados o punteros de función. En estos casos, el concepto semántico de una devolución de llamada se integra en el lenguaje mismo.

C y C ++, por otro lado, no incorporan el concepto semántico de las funciones de devolución de llamada de manera tan explícita. Los mecanismos están ahí, la semántica integrada no. Puede implementar funciones de devolución de llamada perfectamente, pero para obtener algo más sofisticado que incluya una semántica de devolución de llamada explícita, debe construirlo sobre lo que proporciona C ++, como lo que Qt hizo con sus señales y ranuras .

En pocas palabras, C ++ tiene lo que necesita para implementar devoluciones de llamada, a menudo con bastante facilidad y de manera trivial utilizando punteros de función. Lo que no tiene son palabras clave y características cuya semántica sea específica de las devoluciones de llamada, como subir , emitir , controladores , evento + = , etc. Si viene de un lenguaje con ese tipo de elementos, la devolución de llamada nativa es compatible con C ++ se sentirá castrado

Darryl
fuente
1
Afortunadamente, esta no fue la primera respuesta que leí cuando visité esta página, de lo contrario, ¡habría hecho un rebote inmediato!
ubugnu
6

Las funciones de devolución de llamada son parte del estándar C, por lo tanto, también forman parte de C ++. Pero si está trabajando con C ++, le sugiero que utilice el patrón de observador en su lugar: http://en.wikipedia.org/wiki/Observer_pattern

AudioDroid
fuente
1
Las funciones de devolución de llamada no son necesariamente sinónimos de ejecutar una función a través de un puntero de función que se pasó como argumento. Según algunas definiciones, el término función de devolución de llamada lleva la semántica adicional de notificar a otro código de algo que acaba de suceder, o que es hora de que algo suceda. Desde esa perspectiva, una función de devolución de llamada no es parte del estándar C, pero puede implementarse fácilmente utilizando punteros de función, que son parte del estándar.
Darryl
3
"parte del estándar C, y por lo tanto también parte de C ++". Este es un típico malentendido, pero un malentendido :-)
Expiación limitada
Tengo que estar de acuerdo. Lo dejaré como está, ya que solo causará más confusión si lo cambio ahora. Quise decir que el puntero de función (!) Es parte del estándar. Decir algo diferente de eso, estoy de acuerdo, es engañoso.
AudioDroid
¿De qué manera son las funciones de devolución de llamada "parte del estándar C"? No creo que el hecho de que admita funciones y punteros a funciones significa que canoniza específicamente las devoluciones de llamada como un concepto de lenguaje. Además, como se mencionó, eso no sería directamente relevante para C ++, incluso si fuera preciso. Y no es especialmente relevante cuando el OP preguntó "cuándo y cómo" usar devoluciones de llamada en C ++ (una pregunta aburrida, demasiado amplia, pero no obstante), y su respuesta es una advertencia de solo enlace para hacer algo diferente.
underscore_d
4

Consulte la definición anterior donde establece que una función de devolución de llamada se pasa a otra función y en algún momento se llama.

En C ++ es deseable que las funciones de devolución de llamada llamen a un método de clases. Cuando hace esto, tiene acceso a los datos del miembro. Si utiliza la forma C de definir una devolución de llamada, tendrá que señalarla a una función miembro estática. Esto no es muy deseable.

Así es como puede usar devoluciones de llamada en C ++. Asumir 4 archivos. Un par de archivos .CPP / .H para cada clase. La clase C1 es la clase con un método al que queremos devolver la llamada. C2 vuelve a llamar al método de C1. En este ejemplo, la función de devolución de llamada toma 1 parámetro que agregué por el bien de los lectores. El ejemplo no muestra ningún objeto siendo instanciado y utilizado. Un caso de uso para esta implementación es cuando tiene una clase que lee y almacena datos en un espacio temporal y otra que procesa los datos. Con una función de devolución de llamada, por cada fila de datos leídos, la devolución de llamada puede procesarla. Esta técnica corta la sobrecarga del espacio temporal requerido. Es particularmente útil para consultas SQL que devuelven una gran cantidad de datos que luego tienen que ser procesados ​​posteriormente.

/////////////////////////////////////////////////////////////////////
// C1 H file

class C1
{
    public:
    C1() {};
    ~C1() {};
    void CALLBACK F1(int i);
};

/////////////////////////////////////////////////////////////////////
// C1 CPP file

void CALLBACK C1::F1(int i)
{
// Do stuff with C1, its methods and data, and even do stuff with the passed in parameter
}

/////////////////////////////////////////////////////////////////////
// C2 H File

class C1; // Forward declaration

class C2
{
    typedef void (CALLBACK C1::* pfnCallBack)(int i);
public:
    C2() {};
    ~C2() {};

    void Fn(C1 * pThat,pfnCallBack pFn);
};

/////////////////////////////////////////////////////////////////////
// C2 CPP File

void C2::Fn(C1 * pThat,pfnCallBack pFn)
{
    // Call a non-static method in C1
    int i = 1;
    (pThat->*pFn)(i);
}
Gravy Jones
fuente
0

Las señales2 de Boost le permiten suscribir funciones miembro genéricas (¡sin plantillas!) Y de manera segura.

Ejemplo: las señales de vista de documento se pueden usar para implementar arquitecturas flexibles de vista de documento. El documento contendrá una señal a la que cada una de las vistas puede conectarse. La siguiente clase de documento define un documento de texto simple que admite múltiples vistas. Tenga en cuenta que almacena una sola señal a la que se conectarán todas las vistas.

class Document
{
public:
    typedef boost::signals2::signal<void ()>  signal_t;

public:
    Document()
    {}

    /* Connect a slot to the signal which will be emitted whenever
      text is appended to the document. */
    boost::signals2::connection connect(const signal_t::slot_type &subscriber)
    {
        return m_sig.connect(subscriber);
    }

    void append(const char* s)
    {
        m_text += s;
        m_sig();
    }

    const std::string& getText() const
    {
        return m_text;
    }

private:
    signal_t    m_sig;
    std::string m_text;
};

A continuación, podemos comenzar a definir vistas. La siguiente clase TextView proporciona una vista simple del texto del documento.

class TextView
{
public:
    TextView(Document& doc): m_document(doc)
    {
        m_connection = m_document.connect(boost::bind(&TextView::refresh, this));
    }

    ~TextView()
    {
        m_connection.disconnect();
    }

    void refresh() const
    {
        std::cout << "TextView: " << m_document.getText() << std::endl;
    }
private:
    Document&               m_document;
    boost::signals2::connection  m_connection;
};
crizCraig
fuente
0

La respuesta aceptada es exhaustiva pero está relacionada con la pregunta, solo quiero poner un ejemplo simple aquí. Tenía un código que lo había escrito hace mucho tiempo. Quería atravesar un árbol en forma ordenada (nodo izquierdo, luego nodo raíz y luego nodo derecho) y cada vez que alcanzo un Nodo, quería poder llamar a una función arbitraria para que pudiera hacer todo.

void inorder_traversal(Node *p, void *out, void (*callback)(Node *in, void *out))
{
    if (p == NULL)
        return;
    inorder_traversal(p->left, out, callback);
    callback(p, out); // call callback function like this.
    inorder_traversal(p->right, out, callback);
}


// Function like bellow can be used in callback of inorder_traversal.
void foo(Node *t, void *out = NULL)
{
    // You can just leave the out variable and working with specific node of tree. like bellow.
    // cout << t->item;
    // Or
    // You can assign value to out variable like below
    // Mention that the type of out is void * so that you must firstly cast it to your proper out.
    *((int *)out) += 1;
}
// This function use inorder_travesal function to count the number of nodes existing in the tree.
void number_nodes(Node *t)
{
    int sum = 0;
    inorder_traversal(t, &sum, foo);
    cout << sum;
}

 int main()
{

    Node *root = NULL;
    // What These functions perform is inserting an integer into a Tree data-structure.
    root = insert_tree(root, 6);
    root = insert_tree(root, 3);
    root = insert_tree(root, 8);
    root = insert_tree(root, 7);
    root = insert_tree(root, 9);
    root = insert_tree(root, 10);
    number_nodes(root);
}
Ehsan Ahmadi
fuente
1
¿Cómo responde la pregunta?
Rajan Sharma
sabes que la respuesta aceptada es correcta y completa y creo que no hay más cosas que decir en general pero publico un ejemplo de mi uso de las funciones de devolución de llamada.
Ehsan Ahmadi