2009-02-18 20 views
59

Digamos que tiene una función que acepta un puntero void (*)(void*) función para su uso como una devolución de llamada:Al lanzar un puntero de función a otro tipo

void do_stuff(void (*callback_fp)(void*), void* callback_arg); 

Ahora, si tengo una función como esta:

void my_callback_function(struct my_struct* arg); 

¿Puedo hacer esto de forma segura?

do_stuff((void (*)(void*)) &my_callback_function, NULL); 

He mirado en this question y He mirado en algunos estándares de C, que dicen que usted puede echar a 'punteros de función compatibles', pero no puedo encontrar una definición de lo que 'puntero de función compatibles' significa.

+1

Soy un poco novato, pero ¿qué significa un puntero de función "void (*) (void *)"? ¿Es un puntero a una función que acepta un vacío * como argumento y devuelve void –

+1

@Myke: 'void (* func) (void *)' significa que 'func' es un puntero a una función con una firma de tipo como 'void foo (void * arg)'. Entonces sí, tienes razón. – mk12

Respuesta

95

En lo que se refiere al estándar C, si emite un puntero a un puntero a función de un tipo diferente y luego lo llama, es comportamiento indefinido. Ver Anexo J.2 (informativo):

El comportamiento no está definido en las siguientes circunstancias:

  • Un puntero se utiliza para llamar a una función cuyo tipo no es compatible con la punta-a Tipo (6.3.2.3).

Sección 6.3.2.3, párrafo 8 lee:

Un puntero a una función de un tipo se puede convertir en un puntero a una función de otro tipo y de vuelta; el resultado se comparará igual al puntero original. Si se utiliza un puntero convertido para llamar a una función cuyo tipo no es compatible con el tipo apuntado, , el comportamiento no está definido.

En otras palabras, puede convertir un puntero a un tipo de puntero a función diferente, devolverlo nuevamente y llamarlo, y las cosas funcionarán.

La definición de compatible es algo complicada. Se puede encontrar en la sección 6.7.5.3, párrafo 15:

Durante dos tipos de funciones para que sean compatibles, tanto deberá especificar los tipos de retorno compatible .

Además, las listas de tipos de parámetros, si ambas están presentes, coincidirán en el número de parámetros y en el uso del terminador de puntos suspensivos; los parámetros correspondientes deben tener tipos compatibles. Si un tipo tiene una lista de tipos de parámetros y el otro tipo está especificado por un declarador de funciones que no forma parte de una definición de función y que contiene una lista de identificadores vacía, la lista de parámetros no tendrá un terminador de puntos suspensivos y el tipo de cada El parámetro debe ser compatible con el tipo que resulta de la aplicación de las promociones de argumento predeterminadas . Si un tipo tiene una lista de tipos de parámetros y el otro tipo es especificado por una definición de función que contiene una lista de identificadores (posiblemente vacía), ambos deberán aceptar el número de parámetros y el tipo de cada parámetro prototipo será compatible con el tipo que resulta de la aplicación del argumento predeterminado promociones al tipo del identificador correspondiente. (En la determinación de la compatibilidad de tipo y de un tipo compuesto, cada parámetro declarado con tipo de función o matriz se considera que tiene el tipo ajustado y se considera que cada parámetro declarado con el tipo calificado tiene la versión no calificada de su tipo declarado.)

127) Si ambos tipos de funciones son '' estilo antiguo '', los tipos de parámetros no se comparan.

Las reglas para determinar si dos tipos son compatibles se describen en la sección 6.2.7, y no voy a citar aquí ya que son bastante largo, pero se puede leer en la draft of the C99 standard (PDF).

La regla relevante aquí es en el apartado 6.7.5.1, párrafo 2:

Durante dos tipos de puntero para que sean compatibles, ambos serán idénticamente cualificado y ambos serán punteros a tipos compatibles.

Por lo tanto, desde un void* no es compatible con un struct my_struct*, un puntero de función del tipo void (*)(void*) no es compatible con un puntero de función del tipo void (*)(struct my_struct*), por lo que este casting de punteros de función es técnicamente un comportamiento indefinido.

En la práctica, sin embargo, puede salirse con la suya con punteros de función de fundición en algunos casos. En la convención de llamadas x86, los argumentos se insertan en la pila y todos los punteros tienen el mismo tamaño (4 bytes en x86 u 8 bytes en x86_64). Llamar a un puntero de función se reduce a presionar los argumentos en la pila y hacer un salto indirecto al objetivo del puntero de función, y obviamente no hay ninguna noción de tipos en el nivel de código de máquina.

cosas que definitivamente no puede hacer:

  • moldeada entre los punteros de función de diferentes convenciones de llamada.Destruirá la pila y, en el mejor de los casos, chocará, en el peor de los casos, triunfará silenciosamente con un enorme agujero de seguridad. En la programación de Windows, a menudo se pasan punteros a las funciones. Win32 espera que todas las funciones de devolución de llamada utilicen la convención de llamadas stdcall (a la que se extienden las macros CALLBACK, PASCAL y WINAPI). Si pasa un puntero de función que usa la convención de llamada C estándar (cdecl), se generará maldad.
  • En C++, crea entre punteros de función de miembro de clase y punteros de función normales. Esto a menudo hace tropezar a los novatos en C++. Las funciones miembro de clase tienen un parámetro this oculto, y si convierte una función miembro en una función normal, no hay ningún objeto this que usar, y nuevamente, se generará mucha maldad.

Otra mala idea que a veces podría funcionar, pero es también un comportamiento indefinido:

  • Conversiones entre los punteros de función y punteros regulares (por ejemplo, lanzando una void (*)(void) a un void*). Los punteros de función no son necesariamente del mismo tamaño que los punteros regulares, ya que en algunas arquitecturas pueden contener información contextual adicional. Esto probablemente funcionará bien en x86, pero recuerda que es un comportamiento indefinido.
+0

Como señaló el cuestionario, esto solo rechaza un "tipo incompatible". Parece que un tipo de función que toma un vacío * debería ser compatible con un tipo de función que toma cualquier otro tipo de puntero en C, pero la pregunta es qué dice el estándar. – Chuck

+1

@adam, veo que también hiciste tu investigación :) chuck, void * y su tipo de estructura * no son compatibles.entonces es un comportamiento indefinido llamarlo. pero de todos modos, el comportamiento indefinido no siempre es tan malo. si el compilador lo hace bien, ¿por qué preocuparse? pero en este caso, existe una solución limpia. –

+10

¿No es el objetivo de 'void *' que sean compatibles con cualquier otro puntero? No debería haber problema para convertir 'struct my_struct *' en 'void *', de hecho ni siquiera debería tener que lanzar, el compilador debería aceptarlo. Por ejemplo, si pasa una 'struct my_struct *' a una función que toma 'void *', no se requiere conversión. ¿Qué me falta aquí que los hace incompatibles? – brianmearns

4

Como el código C compila a las instrucciones que no se preocupan en absoluto sobre los tipos de puntero, es bastante bueno utilizar el código que menciona. Te topabas con problemas cuando ejecutabas do_stuff con tu función de devolución de llamada y apuntabas a otra cosa, luego mi estructura de estructura como argumento.

espero poder hacerlo más claro al mostrar lo que no funcionaría:

int my_number = 14; 
do_stuff((void (*)(void*)) &my_callback_function, &my_number); 
// my_callback_function will try to access int as struct my_struct 
// and go nuts 

o ...

void another_callback_function(struct my_struct* arg, int arg2) { something } 
do_stuff((void (*)(void*)) &another_callback_function, NULL); 
// another_callback_function will look for non-existing second argument 
// on the stack and go nuts 

Básicamente, puede convertir punteros a lo que quiera, siempre y cuando los datos continúan teniendo sentido en el tiempo de ejecución.

1

Si piensa en la forma en que funcionan las llamadas de función en C/C++, empujan ciertos elementos en la pila, saltan a la nueva ubicación del código, se ejecutan y luego abren la pila al regresar. Si los punteros de función describen funciones con el mismo tipo de devolución y el mismo número/tamaño de argumentos, debería estar bien.

Por lo tanto, creo que debería poder hacerlo de manera segura.

+2

solo está seguro siempre que 'struct'-pointers y' void'-pointers tengan representaciones de bit compatibles; no está garantizado que sea el caso – Christoph

+1

Los compiladores también pueden pasar argumentos en los registros. Y no es extraño usar diferentes registros para flotadores, ints o punteros. – MSalters

4

Tiene un tipo de función compatible si el tipo de devolución y los tipos de parámetros son compatibles, básicamente (es más complicado en realidad :)). La compatibilidad es igual que "del mismo tipo", es más laxa para permitir tener diferentes tipos, pero aún así se puede decir que "estos tipos son casi iguales". En C89, por ejemplo, dos estructuras eran compatibles si fueran idénticas pero su nombre era diferente. C99 parece haber cambiado eso. Citando el c rationale document (lectura muy recomendable, por cierto!):

declaraciones estructura, unión o tipo de enumeración en dos unidades de traducción diferentes no declara formalmente del mismo tipo, incluso si el texto de estas declaraciones provienen de la mismo archivo include, ya que las unidades de traducción son disjuntas. El estándar especifica así las reglas de compatibilidad adicionales para tales tipos, de modo que si dos de tales declaraciones son suficientemente similares, son compatibles.

Eso dijo - sí esto es estrictamente un comportamiento indefinido, debido a que su función do_stuff o alguna otra persona le llame a su función con un puntero de función que tiene void* como parámetro, pero su función tiene un parámetro incompatible. Sin embargo, espero que todos los compiladores compilen y ejecuten sin quejarse. Pero puede hacer más limpia teniendo otra función tomando un void* (y registrar eso como función de devolución de llamada) que luego llamará a su función real.

7

El punto realmente no es si puedes. La solución trivial es

void my_callback_function(struct my_struct* arg); 
void my_callback_helper(void* pv) 
{ 
    my_callback_function((struct my_struct*)pv); 
} 
do_stuff(&my_callback_helper); 

Un buen compilador sólo va a generar código para my_callback_helper si es realmente necesario, en cuyo caso estaría contento de que lo hizo.

+0

El problema es que esta no es una solución general. Debe hacerse caso por caso con conocimiento de la función. Si ya tiene una función del tipo incorrecto, está atascado. – BeeOnRope

0

Los indicadores de vacíos son compatibles con otros tipos de punteros. Es la columna vertebral de cómo funcionan las funciones malloc y mem (memcpy, memcmp). Normalmente, en C (en lugar de C++) NULL es una macro definida como ((void *)0).

Mire 6.3.2.3 (Punto 1) en C99:

Un puntero para anular se puede convertir en o desde un puntero a cualquier incompleta o objeto de tipo

+0

Esto contradice a [respuesta de Adam Rosenfield] (http://stackoverflow.com/a/559671), vea el último párrafo y comentarios – user

+0

Esta respuesta es claramente incorrecta. Cualquier puntero es convertible a un puntero de vacío *, excepto los punteros de función *. – marton78

24

pregunté por este mismo problema exacto con respecto a un cierto código en GLib recientemente. (GLib es una biblioteca central para el proyecto GNOME y está escrita en C.) Me dijeron que todo el marco de señales de los tragamonedas depende de ello.

en todo el código, hay numerosos casos de fundición de tipo (1) a (2):

  1. typedef int (*CompareFunc) (const void *a, const void *b)
  2. typedef int (*CompareDataFunc) (const void *b, const void *b, void *user_data)

es común a la cadena Seguido con llamadas como este:

int stuff_equal (GStuff  *a, 
       GStuff  *b, 
       CompareFunc compare_func) 
{ 
    return stuff_equal_with_data(a, b, (CompareDataFunc) compare_func, NULL); 
} 

int stuff_equal_with_data (GStuff   *a, 
          GStuff   *b, 
          CompareDataFunc compare_func, 
          void   *user_data) 
{ 
    int result; 
    /* do some work here */ 
    result = compare_func (data1, data2, user_data); 
    return result; 
} 

Véalo usted mismo en g_array_sort(): http://git.gnome.org/browse/glib/tree/glib/garray.c

Las respuestas anteriores son detallados y probablemente correcta - si se siente en el comité de estándares. Adam y Johannes merecen crédito por sus respuestas bien documentadas. Sin embargo, en la naturaleza, encontrarás que este código funciona bien. ¿Polémico? Sí. Considere esto: GLib compila/trabaja/prueba en un gran número de plataformas (Linux/Solaris/Windows/OS X) con una amplia variedad de compiladores/enlazadores/cargadores de kernel (GCC/CLang/MSVC). Los estándares están malditos, supongo.

Pasé un tiempo pensando en estas respuestas. Aquí está mi conclusión:

  1. Si está escribiendo una biblioteca de devolución de llamada, esto podría estar bien. Caveat Emptor: utilícelo bajo su propio riesgo.
  2. De lo contrario, no lo hagas.

Pensando más profundo después de escribir esta respuesta, no me sorprendería si el código para los compiladores de C usa este mismo truco. Y dado que (¿la mayoría/todos?) Los compiladores de C modernos son bootstrap, esto implicaría que el truco es seguro.

Una pregunta más importante para investigar: ¿Alguien puede encontrar una plataforma/compilador/enlazador/cargador donde este truco hace no? Puntos importantes de brownie para eso. Apuesto a que hay algunos procesadores/sistemas integrados a los que no les gusta. Sin embargo, para computadoras de escritorio (y probablemente para dispositivos móviles/tabletas), este truco probablemente todavía funcione.

+7

Un lugar donde definitivamente no funciona es el compilador Emscripten LLVM a Javascript. Consulte https://github.com/kripken/emscripten/wiki/Asm-pointer-casts para obtener más detalles. –

+2

Referencia duplicada sobre [Emscripten] (http://kripken.github.io/emscripten-site/docs/porting/guidelines/function_pointer_issues.html#asm-pointer-casts). – ysdx

Cuestiones relacionadas