Digamos que tengo una función que acepta un void (*)(void*)
puntero de función para usar como devolución de llamada:
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 examinado esta pregunta y he examinado algunos estándares de C que dicen que puede convertir a 'punteros de función compatibles', pero no puedo encontrar una definición de lo que significa 'puntero de función compatible'.
c
function-pointers
Mike Weller
fuente
fuente
void (*func)(void *)
significa quefunc
es un puntero a una función con una firma de tipo comovoid foo(void *arg)
. Entonces sí, tienes razón.Respuestas:
En lo que respecta al estándar C, si lanza un puntero de función a un puntero de función de un tipo diferente y luego lo llama, es un comportamiento indefinido . Ver Anexo J.2 (informativo):
La sección 6.3.2.3, párrafo 8 dice:
En otras palabras, puede convertir un puntero de función a un tipo de puntero de función diferente, devolverlo de nuevo y llamarlo, y todo funcionará.
La definición de compatible es algo complicada. Se puede encontrar en la sección 6.7.5.3, párrafo 15:
Las reglas para determinar si dos tipos son compatibles se describen en la sección 6.2.7, y no las citaré aquí porque son bastante extensas, pero puede leerlas en el borrador del estándar C99 (PDF) .
La regla relevante aquí está en la sección 6.7.5.1, párrafo 2:
Por lo tanto, dado que a
void*
no es compatible con astruct my_struct*
, un puntero de función de tipovoid (*)(void*)
no es compatible con un puntero de función de tipovoid (*)(struct my_struct*)
, por lo que esta conversión de punteros de función es un comportamiento técnicamente indefinido.Sin embargo, en la práctica, en algunos casos puede salirse con la suya sin problemas con los punteros de función de lanzamiento. 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 empujar los argumentos en la pila y hacer un salto indirecto al destino del puntero de función, y obviamente no hay noción de tipos a nivel de código de máquina.
Cosas que definitivamente no puedes hacer:
stdcall
convención de llamada (que las macrosCALLBACK
,PASCAL
yWINAPI
todo se expanden a). Si pasa un puntero de función que usa la convención de llamada estándar de C (cdecl
), resultará mal.this
parámetro oculto , y si convierte una función miembro a una función normal, no hay ningúnthis
objeto para usar y, nuevamente, resultará en mucha maldad.Otra mala idea que a veces puede funcionar, pero también es un comportamiento indefinido:
void (*)(void)
a avoid*
). Los punteros de función no tienen necesariamente el mismo tamaño que los punteros normales, ya que en algunas arquitecturas pueden contener información contextual adicional. Esto probablemente funcionará bien en x86, pero recuerde que es un comportamiento indefinido.fuente
void*
trata de que son compatibles con cualquier otro puntero? No debería haber ningún problema para convertir astruct my_struct*
a avoid*
, de hecho, ni siquiera debería tener que emitir, el compilador debería aceptarlo. Por ejemplo, si pasa astruct my_struct*
a una función que toma avoid*
, no se requiere conversión. ¿Qué me estoy perdiendo aquí que los hace incompatibles?void*
tipos de puntero de función, consulte las especificaciones .void *
solo es "compatible con" cualquier otro puntero (sin función) de formas definidas con mucha precisión (que no están relacionadas con lo que el estándar C significa con la palabra "compatible" en este caso). C permite quevoid *
a sea más grande o más pequeño que astruct my_struct *
, o tener los bits en un orden diferente o negados o lo que sea. Entonces,void f(void *)
yvoid f(struct my_struct *)
puede ser incompatible con ABI . C convertirá los punteros por usted si es necesario, pero no lo hará y, a veces, no podría convertir una función apuntada para que tome un tipo de argumento posiblemente diferente.Pregunté sobre este mismo problema con respecto a algún 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 slots'n'signals depende de ello.
A lo largo del código, existen numerosos casos de conversión del tipo (1) al (2):
typedef int (*CompareFunc) (const void *a, const void *b)
typedef int (*CompareDataFunc) (const void *b, const void *b, void *user_data)
Es común encadenar con llamadas como esta:
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 aquí en
g_array_sort()
: http://git.gnome.org/browse/glib/tree/glib/garray.cLas respuestas anteriores son detalladas y probablemente correctas, si forma parte del comité de normas. Adam y Johannes merecen crédito por sus respuestas bien investigadas. Sin embargo, en la naturaleza, encontrará que este código funciona bien. ¿Polémico? Si. Considere esto: GLib compila / trabaja / prueba en una gran cantidad de plataformas (Linux / Solaris / Windows / OS X) con una amplia variedad de compiladores / enlazadores / cargadores de kernel (GCC / CLang / MSVC). Al diablo con los estándares, supongo.
Pasé algún tiempo pensando en estas respuestas. Aquí está mi conclusión:
Pensando más profundamente 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: ¿Puede alguien encontrar una plataforma / compilador / enlazador / cargador donde este truco funciona? no funcione? Grandes puntos de brownie para ese. Apuesto a que hay algunos procesadores / sistemas integrados a los que no les gusta. Sin embargo, para computadoras de escritorio (y probablemente móviles / tabletas), este truco probablemente aún funcione.
fuente
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 solo generará código para my_callback_helper si es realmente necesario, en cuyo caso te alegrarás de que así sea.
fuente
my_callback_helper
, a menos que siempre esté en línea. Definitivamente, esto no es necesario, ya que lo único que suele hacer esjmp my_callback_function
. El compilador probablemente quiera asegurarse de que las direcciones de las funciones sean diferentes, pero desafortunadamente lo hace incluso cuando la función está marcada con C99inline
(es decir, "no me importa la dirección").void *
puede ser incluso de diferente tamaño que astruct *
(creo que eso es incorrecto, porque de lo contrariomalloc
estaría roto, pero ese comentario tiene 5 votos a favor, así que le doy algo de crédito. Si @mtraceur es correcto, la solución que escribió no sería correcto.void*
todavía tiene que funcionar. En resumen,void*
puede tener más bits, pero si lanzastruct*
avoid*
esos bits adicionales pueden ser ceros y la conversión puede simplemente descartar esos ceros nuevamente.void *
podría (en teoría) ser tan diferente de unastruct *
. Estoy implementando una vtable en C, y estoy usando unthis
puntero C ++ - ish como el primer argumento para las funciones virtuales. Obviamente,this
debe ser un puntero a la estructura "actual" (derivada). Entonces, las funciones virtuales necesitan diferentes prototipos dependiendo de la estructura en la que se implementan. Pensé que usar unvoid *this
argumento arreglaría todo, pero ahora aprendí que es un comportamiento indefinido ...Tiene un tipo de función compatible si el tipo de retorno y los tipos de parámetros son compatibles, básicamente (es más complicado en realidad :)). La compatibilidad es la misma que "mismo tipo", pero más laxa para permitir tener diferentes tipos, pero aún así tener alguna forma de decir "estos tipos son casi iguales". En C89, por ejemplo, dos estructuras eran compatibles si eran idénticas pero su nombre era diferente. C99 parece haber cambiado eso. Citando del documento de fundamento c (lectura muy recomendable, por cierto!):
Dicho esto, sí, estrictamente, este es un comportamiento indefinido, porque su función do_stuff o alguien más llamará a su función con un puntero de función que tenga
void*
como parámetro, pero su función tiene un parámetro incompatible. Sin embargo, espero que todos los compiladores lo compilen y lo ejecuten sin quejarse. Pero puede hacerlo más limpio si tiene otra función que toma unvoid*
(y lo registra como función de devolución de llamada) que simplemente llamará a su función real en ese momento.fuente
Como el código C se compila con instrucciones que no se preocupan en absoluto por los tipos de punteros, está bastante bien usar el código que menciona. Tendría problemas cuando ejecutara do_stuff con su función de devolución de llamada y el puntero a otra cosa que no sea la estructura my_struct como argumento.
Espero poder hacerlo más claro mostrando 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 lanzar punteros a lo que quiera, siempre que los datos sigan teniendo sentido en tiempo de ejecución.
fuente
Si piensa en la forma en que funcionan las llamadas a funciones en C / C ++, empujan ciertos elementos en la pila, saltan a la nueva ubicación del código, ejecutan y luego abren la pila al regresar. Si los punteros de su función describen funciones con el mismo tipo de retorno y el mismo número / tamaño de argumentos, debería estar bien.
Por lo tanto, creo que debería poder hacerlo de forma segura.
fuente
struct
-pointers yvoid
-pointers tengan representaciones de bits compatibles; no se garantiza que ese sea el casoLos punteros vacíos son compatibles con otros tipos de punteros. Es la columna vertebral de cómo funcionan malloc y las funciones mem (
memcpy
,memcmp
). Normalmente, en C (en lugar de C ++)NULL
hay una macro definida como((void *)0)
.Mire 6.3.2.3 (elemento 1) en C99:
fuente