¿Debo usar std:: function o un puntero de función en C++?


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 debo hacer uso de std::función:

std::function< void(int) > callbackFunc;
Author: Vertexwahn, 2014-09-15

5 answers

En resumen, el uso std::function a menos que tengas una razón para no hacerlo.

Los punteros de función tienen la desventaja de no ser capaces de capturar algún contexto. Por ejemplo, no podrá pasar una función lambda como devolución de llamada que capture algunas variables de contexto (pero funcionará si no captura ninguna). Llamar a una variable miembro de un objeto (es decir, no estática) tampoco es posible, ya que el objeto (this - puntero) debe ser capturar.(1)

std::function (dado 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, es probablemente 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 llama (por lo que en una situación muy crítica de rendimiento podría ser un problema, pero en la mayoría no debería). Es muy "universal": si te importa mucho el código consistente y legible, así como no quieres pensar en cada elección que haces (es decir, quieres mantenerlo simple), usa std::function para cada función que pases.

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 llamable , es decir, un puntero de función, una functor, a 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 se obtiene la ventaja de que la llamada a la devolución de llamada puede ser inlineada, ya que el código de cliente de su función (externa) "ve" la llamada a la devolución de llamada será la información de tipo exacto que está disponible.

Ejemplo para la versión con el parámetro template (escribe & en lugar de && para pre-C++11):

template <typename CallbackFunction>
void myFunction(..., CallbackFunction && callback) {
    ...
    callback(...);
    ...
}

As se puede ver en la siguiente tabla, todos ellos 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 alternativas para superar esta limitación, por ejemplo, pasando los datos adicionales como parámetros adicionales a su función (externa): myFunction(..., callback, data) llamará a callback(data). Ese es el estilo C "callback con argumentos", que es posible en C++ (y por cierto muy utilizado en la API de 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 la función decide el tipo del 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

 142
Author: leemes,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2014-09-15 23:48:38

void (*callbackFunc)(int); puede ser una función de devolución de llamada estilo C, pero es una horriblemente inutilizable de diseño pobre.

Una devolución de llamada estilo C bien diseñada se parece a void (*callbackFunc)(void*, int); has tiene un void* para permitir que el código que hace la devolución de llamada mantenga el 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 descortés.

std::function< int(int) > termina siendo un poco más caro que la invocación int(*)(void*, int) en la mayoría de las implementaciones. Sin embargo, es más difícil para algunos compiladores en línea. Hay implementaciones de clones std::function que rivalizan con los gastos generales de invocación de puntero de función (ver 'delegados más rápidos posibles', etc.) que pueden abrirse camino en 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 ser conscientes de la vida útil de la devolución de llamada. void(*callback)(void*, int) no proporciona esto.

A veces esto está disponible a través de la estructura de código (la devolución de llamada tiene una vida útil limitada) o a través de otros mecanismos (desregistrar callbacks y similares).

std::function proporciona un medio para la gestión de vida limitada (la última copia del objeto desaparece cuando se olvida).

En general, usaría un std::function a menos que se manifiesten preocupaciones de rendimiento. Si lo hicieran, primero buscaría cambios estructurales (en lugar de una devolución de llamada por píxel, ¿qué tal generar un procesador scanline basado en la lambda que me pasas? que debería ser suficiente para reducir la sobrecarga de llamadas a funciones a trivial nivel.). Entonces, si persiste, escribiría un delegate basado en delegados más rápidos posibles, y ver 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 comunicarse entre diferentes compiladores de código generado. También los he utilizado como detalles de implementación interna cuando estoy implementando tablas de salto, borrado de tipos, etc.: cuando lo estoy produciendo y consumiendo, y no lo estoy exponiendo externamente para ningún código de cliente para usar, y punteros de función hacen todo lo que necesito.

Tenga en cuenta que puede escribir envoltorios que conviertan un std::function<int(int)> en un callback de estilo int(void*,int), suponiendo que haya una infraestructura de gestión de vida útil de callback adecuada. Entonces, como prueba de humo para cualquier sistema de gestión de vida de devolución de llamada estilo C, me aseguraría de que envolver un std::function funcione razonablemente bien.

 22
Author: Yakk - Adam Nevraumont,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2016-05-30 11:42:38

Use std::function para almacenar objetos llamables arbitrarios. Permite al usuario proporcionar cualquier contexto 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 argumento void * user_context para que sea al menos posible (aunque inconveniente) que tenga acceso al estado que no se pasa directamente a la función.

 16
Author: Mike Seymour,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2014-09-15 13:13:02

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 el soporte del lenguaje pre-C++11 no es un requisito, usar std::function le da a sus llamantes más opciones para implementar la devolución de llamada, lo que lo 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, mientras abstrae los detalles de su implementación para su código que realiza la devolución de llamada.

 13
Author: dasblinkenlight,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2014-09-15 13:03:06

std::function puede llevar VMT al código en algunos casos, lo que tiene cierto impacto en el rendimiento.

 0
Author: vladon,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-01-20 11:55:50