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

142

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;
Jan Swart
fuente
9
Si la función de devolución de llamada se conoce en tiempo de compilación, considere una plantilla en su lugar.
Baum mit Augen
44
Al implementar una función de devolución de llamada, debe hacer lo que la persona que llama requiera. Si su pregunta es realmente sobre el diseño de una interfaz de devolución de llamada, aquí no hay suficiente información para responderla. ¿Qué desea que haga el destinatario de su devolución de llamada? ¿Qué información necesita pasar al destinatario? ¿Qué información debe devolverle el destinatario como resultado de la llamada?
Pete Becker

Respuestas:

171

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::functionpara 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

Leemes
fuente
9
los punteros de función tienen sobrecarga de llamadas en comparación con los parámetros de la plantilla Los parámetros de plantilla hacen que la alineación sea fácil, incluso si se pasa a niveles superiores, porque el código que se ejecuta se describe por el tipo de parámetro, no por el valor. Y los objetos de función de plantilla que se almacenan en tipos de retorno de plantilla son un patrón común y útil (con un buen constructor de copia, puede crear la función de plantilla eficiente invocable que se puede convertir a la de std::functiontipo borrado si necesita almacenarla fuera del llamado contexto).
Yakk - Adam Nevraumont
1
@tohecz Ahora menciono si requiere C ++ 11 o no.
leemes
1
@Yakk ¡Oh, por supuesto, se olvidó de eso! Lo agregué, gracias.
leemes
1
@MooingDuck Por supuesto, depende de la implementación. Pero si recuerdo correctamente, debido a cómo funciona el borrado de tipo, ¿se está produciendo una indirección más? Pero ahora que lo pienso de nuevo, creo que esto no es el caso si los punteros de función Asignar o captura en menos lambdas a que ... (como una optimización típico)
leemes
1
@leemes: Correcto, para punteros de función o lambdas sin captura, debe tener la misma sobrecarga que un c-func-ptr. Que todavía es un puesto de tubería + no trivialmente en línea.
Mooing Duck
25

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::functionimplementaciones 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::functionmanifiesto 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 delegatedelegado 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::functionfuncione razonablemente bien.

Yakk - Adam Nevraumont
fuente
1
¿De dónde vino esto 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.
Nikos
@ nik-lz No estoy seguro de cómo enseñarle el uso y el historial de devoluciones de llamada en C en un comentario. O la filosofía de la programación procesal en oposición a la programación funcional. Entonces, te irás sin cumplir.
Yakk - Adam Nevraumont
Se me olvidó this. ¿Es porque uno tiene que tener en cuenta el caso de una función miembro que se llama, por lo que necesitamos el thispuntero 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.
Nikos
@ Las funciones miembro de Nik-Lz no son funciones. Las funciones no tienen estado (tiempo de ejecución). Las devoluciones de llamada toman un 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".
Yakk - Adam Nevraumont
Sí, de eso estaba hablando. El estado de tiempo de ejecución es básicamente la dirección del objeto que se llama (porque cambia entre ejecuciones). Todavía se trata this. A eso me refería. Ok, gracias de todos modos.
Nikos
17

Se usa std::functionpara 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_contextargumento para que al menos sea posible (aunque sea inconveniente) que acceda al estado que no se pasa directamente al función.

Mike Seymour
fuente
¿Cuál es el tipo de p aquí? ¿será un tipo de función std ::? nulo f () {}; auto p = f; pags();
sree
14

La única razón para evitar std::functiones 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::functionle 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.

dasblinkenlight
fuente
1

std::function puede traer VMT al código en algunos casos, lo que tiene algún impacto en el rendimiento.

vladon
fuente
3
¿Puedes explicarme qué es este VMT?
Gupta