Al implementar una función de devolución de llamada en C ++, ¿debo seguir usando el puntero de función de estilo C:
void (*callbackFunc)(int);
¿O debería hacer uso de std :: function:
std::function< void(int) > callbackFunc;
Al implementar una función de devolución de llamada en C ++, ¿debo seguir usando el puntero de función de estilo C:
void (*callbackFunc)(int);
¿O debería hacer uso de std :: function:
std::function< void(int) > callbackFunc;
Respuestas:
En resumen, úselo astd::function
menos que tenga una razón para no hacerlo.
Los punteros de función tienen la desventaja de no poder capturar algún contexto. No podrá, por ejemplo, pasar una función lambda como devolución de llamada que capture algunas variables de contexto (pero funcionará si no captura ninguna). Por lo tanto, llamar a una variable miembro de un objeto (es decir, no estático) tampoco es posible, ya que el objeto ( this
-pointer) necesita ser capturado. (1)
std::function
(ya que C ++ 11) es principalmente para almacenar una función (pasarla no requiere que se almacene). Por lo tanto, si desea almacenar la devolución de llamada, por ejemplo, en una variable miembro, probablemente sea su mejor opción. Pero también si no lo almacena, es una buena "primera opción", aunque tiene la desventaja de introducir algunos gastos generales (muy pequeños) cuando se lo llama (por lo que en una situación muy crítica de rendimiento puede ser un problema, pero en la mayoría no debería). Es muy "universal": si le importa mucho el código coherente y legible, así como no quiere pensar en cada elección que haga (es decir, quiere que sea simple), úselo std::function
para cada función que pase.
Piense en una tercera opción: si está a punto de implementar una pequeña función que luego informa algo a través de la función de devolución de llamada proporcionada, considere un parámetro de plantilla , que luego puede ser cualquier objeto invocable , es decir, un puntero de función, un functor, un lambda, a std::function
, ... El inconveniente aquí es que su función (externa) se convierte en una plantilla y, por lo tanto, debe implementarse en el encabezado. Por otro lado, tiene la ventaja de que la llamada a la devolución de llamada se puede incluir en línea, ya que el código del cliente de su función (externa) "ve" la llamada a la devolución de llamada con la información del tipo exacto disponible.
Ejemplo para la versión con el parámetro de plantilla (escriba en &
lugar de &&
para pre-C ++ 11):
template <typename CallbackFunction>
void myFunction(..., CallbackFunction && callback) {
...
callback(...);
...
}
Como puede ver en la siguiente tabla, todos tienen sus ventajas y desventajas:
+-------------------+--------------+---------------+----------------+
| | function ptr | std::function | template param |
+===================+==============+===============+================+
| can capture | no(1) | yes | yes |
| context variables | | | |
+-------------------+--------------+---------------+----------------+
| no call overhead | yes | no | yes |
| (see comments) | | | |
+-------------------+--------------+---------------+----------------+
| can be inlined | no | no | yes |
| (see comments) | | | |
+-------------------+--------------+---------------+----------------+
| can be stored | yes | yes | no(2) |
| in class member | | | |
+-------------------+--------------+---------------+----------------+
| can be implemented| yes | yes | no |
| outside of header | | | |
+-------------------+--------------+---------------+----------------+
| supported without | yes | no(3) | yes |
| C++11 standard | | | |
+-------------------+--------------+---------------+----------------+
| nicely readable | no | yes | (yes) |
| (my opinion) | (ugly type) | | |
+-------------------+--------------+---------------+----------------+
(1) Existen soluciones para superar esta limitación, por ejemplo, pasar los datos adicionales como parámetros adicionales a su función (externa): myFunction(..., callback, data)
llamará callback(data)
. Esa es la "devolución de llamada con argumentos" de estilo C, que es posible en C ++ (y, por cierto, muy utilizada en la API WIN32) pero debe evitarse porque tenemos mejores opciones en C ++.
(2) A menos que estemos hablando de una plantilla de clase, es decir, la clase en la que almacena la función es una plantilla. Pero eso significaría que en el lado del cliente, el tipo de función decide el tipo de objeto que almacena la devolución de llamada, que casi nunca es una opción para casos de uso reales.
(3) Para pre-C ++ 11, use boost::function
std::function
tipo borrado si necesita almacenarla fuera del llamado contexto).
void (*callbackFunc)(int);
puede ser una función de devolución de llamada de estilo C, pero es horriblemente inutilizable con un diseño deficiente.
Parece una devolución de llamada de estilo C bien diseñada void (*callbackFunc)(void*, int);
: tiene void*
que permitir que el código que realiza la devolución de llamada mantenga un estado más allá de la función. No hacer esto obliga a la persona que llama a almacenar el estado a nivel mundial, lo cual es de mala educación.
std::function< int(int) >
termina siendo un poco más caro que la int(*)(void*, int)
invocación en la mayoría de las implementaciones. Sin embargo, es más difícil para algunos compiladores en línea. Hay std::function
implementaciones de clones que rivalizan con los gastos generales de invocación de puntero (ver 'delegados más rápidos posibles', etc.) que pueden llegar a las bibliotecas.
Ahora, los clientes de un sistema de devolución de llamada a menudo necesitan configurar recursos y disponer de ellos cuando se crea y elimina la devolución de llamada, y estar al tanto de la duración de la devolución de llamada. void(*callback)(void*, int)
no proporciona esto
A veces, esto está disponible a través de la estructura del código (la devolución de llamada tiene una vida útil limitada) o mediante otros mecanismos (cancelar el registro de devoluciones de llamada y similares).
std::function
proporciona un medio para una gestión limitada de por vida (la última copia del objeto desaparece cuando se olvida).
En general, usaría un std::function
manifiesto a menos que se trate de problemas de rendimiento Si lo hicieran, primero buscaría cambios estructurales (en lugar de una devolución de llamada por píxel, ¿qué tal si generamos un procesador de línea de escaneo basado en el lambda que me pasa? Que debería ser suficiente para reducir la sobrecarga de llamadas de función a niveles triviales. ) Luego, si persiste, escribiría un delegate
delegado basado en el más rápido posible y vería si el problema de rendimiento desaparece.
En su mayoría, solo usaría punteros de función para API heredadas, o para crear interfaces C para comunicar entre diferentes compiladores generados por código. También los he usado como detalles de implementación interna cuando estoy implementando tablas de salto, borrado de texto, etc.: cuando lo estoy produciendo y consumiendo, y no lo expongo externamente para que cualquier código de cliente lo use, y los punteros de función hacen todo lo que necesito .
Tenga en cuenta que puede escribir contenedores que conviertan un std::function<int(int)>
en una int(void*,int)
devolución de llamada de estilo, suponiendo que exista una infraestructura adecuada de administración de por vida de devolución de llamada. Por lo tanto, como prueba de humo para cualquier sistema de administración de por vida de devolución de llamada de estilo C, me aseguraría de que la envoltura std::function
funcione razonablemente bien.
void*
? ¿Por qué querrías mantener un estado más allá de la función? Una función debe contener todo el código que necesita, toda la funcionalidad, solo debe pasarle los argumentos deseados y modificar y devolver algo. Si necesita algún estado externo, ¿por qué una functionPtr o una devolución de llamada llevarían ese equipaje? Creo que la devolución de llamada es innecesariamente compleja.
this
. ¿Es porque uno tiene que tener en cuenta el caso de una función miembro que se llama, por lo que necesitamos el this
puntero para apuntar a la dirección del objeto? Si me equivoco, ¿podría darme un enlace donde pueda encontrar más información sobre esto, porque no puedo encontrar mucho al respecto? Gracias por adelantado.
void*
para permitir la transmisión del estado de tiempo de ejecución. Un puntero de función con ay void*
un void*
argumento puede emular una llamada de función miembro a un objeto. Lo siento, no conozco un recurso que pase por "el diseño de mecanismos de devolución de llamada C 101".
this
. A eso me refería. Ok, gracias de todos modos.
Se usa std::function
para almacenar objetos invocables arbitrarios. Permite al usuario proporcionar el contexto que sea necesario para la devolución de llamada; un puntero de función simple no lo hace.
Si necesita usar punteros de función simples por alguna razón (quizás porque desea una API compatible con C), entonces debe agregar un void * user_context
argumento para que al menos sea posible (aunque sea inconveniente) que acceda al estado que no se pasa directamente al función.
La única razón para evitar std::function
es el soporte de compiladores heredados que carecen de soporte para esta plantilla, que se ha introducido en C ++ 11.
Si admitir el lenguaje anterior a C ++ 11 no es un requisito, el uso std::function
le da a las personas que llaman más opciones para implementar la devolución de llamada, lo que la convierte en una mejor opción en comparación con los punteros de función "simples". Ofrece a los usuarios de su API más opciones, al tiempo que resume los detalles de su implementación para su código que realiza la devolución de llamada.