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.
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.
Respuestas:
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, elfor_each
algoritmo aplica una devolución de llamada unaria a cada elemento en un rango de iteradores:que se puede utilizar para incrementar primero y luego imprimir un vector al pasar callables apropiados, por ejemplo:
que imprime
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:
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á.
Aquí la función
key_pressed
utiliza las devoluciones de llamada almacenadasactions
para obtener el comportamiento deseado cuando se presiona una tecla determinada. Si el jugador elige cambiar el botón para saltar, el motor puede llamary, por lo tanto, cambia el comportamiento de una llamada a
key_pressed
(a la que se llamaplayer_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 * :
std::function
objetosoperator()
)* 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
Nota: A partir de C ++ 17,
f(...)
se puede escribir una llamada comostd::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
:1.1 Escribir un puntero de función / notación de tipo
Un tipo de puntero de función tiene la notación
donde se verá un tipo de puntero de función con nombre
La
using
declaración nos da la opción de hacer las cosas un poco más legibles, ya quetypedef
forf_int_t
también se puede escribir como:Donde (al menos para mí) es más claro cuál es
f_int_t
el nuevo alias de tipo y el reconocimiento del tipo de puntero de función también es más fácilY una declaración de una función que utiliza una devolución de llamada del tipo de puntero de función será:
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:
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:
1.4 Ejemplo
Se puede escribir una función que no dependa de cómo funciona la devolución de llamada:
donde podrían ser posibles devoluciones de llamada
utilizado como
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 tipoC
para operar.2.1 Escribir puntero a la función miembro / notación de tipo
Un puntero al tipo de función miembro para alguna clase
T
tiene la notacióndonde un puntero con nombre a la función miembro , en analogía al puntero de la función, se verá así:
Ejemplo: declarar una función tomando un puntero a la devolución de llamada de la función miembro como uno de sus argumentos:
2.2 Notación de devolución de llamada
Se
C
puede invocar la función de puntero a miembro de , con respecto a un objeto de tipoC
mediante operaciones de acceso de miembro en el puntero desreferenciado. Nota: ¡Se requiere paréntesis!Nota: Si
C
hay disponible un puntero a, la sintaxis es equivalente (donde también seC
debe desreferenciar el puntero a ):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
T
se puede llamar usando un puntero de clase miembro de claseT
.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:
3.
std::function
objetos (encabezado<functional>
)La
std::function
clase es un contenedor de funciones polimórficas para almacenar, copiar o invocar invocables.3.1 Escribir una
std::function
notación de objeto / tipoEl tipo de un
std::function
objeto que almacena un invocable se ve así:3.2 Notación de llamada de devolución de llamada
La clase
std::function
haoperator()
definido cuál puede usarse para invocar su objetivo.3.3 Notación de uso de devolución de llamada y tipos compatibles
La
std::function
devolució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 unstd::function
objeto.3.3.1 Punteros de función y punteros a funciones miembro
Un puntero de función
o un puntero a la función miembro
puede ser usado.
3.3.2 Expresiones lambda
Un cierre sin nombre de una expresión lambda se puede almacenar en un
std::function
objeto:3.3.3
std::bind
expresionesSe
std::bind
puede pasar el resultado de una expresión. Por ejemplo, vinculando parámetros a una llamada de puntero de función:Donde también los objetos pueden vincularse como el objeto para la invocación de las funciones de puntero a miembro:
3.3.4 Objetos de función
Los objetos de clases que tienen una
operator()
sobrecarga adecuada también se pueden almacenar dentro de unstd::function
objeto.3.4 Ejemplo
Cambiar el ejemplo del puntero de función para usar
std::function
le da mucha más utilidad a esa función porque (ver 3.3) tenemos más posibilidades de usarla:
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::function
objetos.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_int
código de arriba aún más se puede lograr mediante el uso de plantillas: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:
Nota: La salida incluida imprime el nombre del tipo deducido para el tipo de plantilla
F
. La implementación detype_name
se 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.4.2 Ejemplos que utilizan devoluciones de llamada con plantillas y tipos compatibles
Los tipos compatibles para el
std::function
método de devolución de llamada con plantillastdf_transform_every_int_templ
son 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:
Nota:
std_ftransform_every_int
(versión sin plantilla; ver arriba) funcionafoo
pero no se usamuh
.El parámetro simple con plantilla de
transform_every_int_templ
puede ser cualquier tipo invocable posible.Se imprime el código anterior:
type_name
implementación utilizada anteriormentefuente
int b = foobar(a, foo); // call foobar with pointer to foo as callback
, este es un error tipográfico ¿verdad?foo
debería ser un puntero para que esto funcione AFAIK.[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í.También existe la forma C de hacer devoluciones de llamada: punteros de función
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:
fuente
typedef
el tipo de devolución de llamada? ¿Es posible?typedef
es solo azúcar sintáctico para hacerlo más legible. Sintypedef
, 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))
Scott Meyers da un buen ejemplo:
Creo que el ejemplo lo dice todo.
std::function<>
es la forma "moderna" de escribir devoluciones de llamada de C ++.fuente
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.
fuente
La respuesta aceptada es muy útil y bastante completa. Sin embargo, el OP establece
Así que aquí tienes, desde C ++ 11 que tienes,
std::function
así que no hay necesidad de punteros de función y cosas similares:Por cierto, este ejemplo es real, porque desea llamar a la función
print_hashes
con 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 esstd::function<int (const std::string&)>
que describe dicha función como un argumento de entrada de la función que la invocará.fuente
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
fuente
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
fuente
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.
fuente
Las señales2 de Boost le permiten suscribir funciones miembro genéricas (¡sin plantillas!) Y de manera segura.
fuente
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.
fuente