Pregunta

Vamos a decir que tengo una función que acepta un puntero de función void (*)(void*) para su uso como una 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 con seguridad?

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

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

¿Fue útil?

Solución

En lo que se refiere al estándar de C, si lanzas un puntero de función a un puntero de función de un tipo diferente y luego llamas a eso, 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 dice lo siguiente:

  

Un puntero a una función de un tipo se puede convertir en un puntero a una función de otro   escriba y de vuelta; el resultado se compara igual a la original del puntero. Si un convertida   puntero se utiliza para llamar a una función cuyo tipo no es compatible con el cual apunta a tipo,   el comportamiento es indefinido.

Así, en otras palabras, se puede convertir un puntero de función a un tipo de puntero de función diferente, echarlo hacia atrás de nuevo, y lo llaman, y las cosas van a trabajar.

La definición de compatibles es algo complicado. 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 compatibles 127 .

     

Además, las listas de tipos de parámetros, si ambos están presentes, se están de acuerdo en el número de   parámetros y en el uso del terminador de puntos suspensivos; parámetros correspondientes tendrán   tipos compatibles. Si un tipo tiene una lista tipo de parámetro y el otro tipo se especifica mediante una   declarador función que no es parte de una definición de función y que contiene un vacío   lista de identificadores, la lista de parámetros no tendrá un terminador de puntos suspensivos y el tipo de cada   parámetro deberá ser compatible con el tipo que resulta de la aplicación de la   promociones argumento predeterminado. Si un tipo tiene una lista tipo de parámetro y el otro tipo es   especificado por una definición de función que contiene una lista de identificadores (posiblemente vacío), tanto deberá   están de acuerdo en 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 tipo   compatibilidad y de un tipo de material compuesto, cada parámetro declarado con la función o la matriz   tipo se toma como que tiene el tipo ajustado y cada parámetro declarado con el tipo calificado   se toma como teniendo la versión sin reservas de su tipo declarado.)

     

127) Si los dos tipos de función son ‘‘viejo estilo’’, 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 borrador de la norma C99 (PDF) .

La regla aquí es relevante en la sección 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 comportamiento indefinido.

En la práctica, sin embargo, se puede obtener de forma segura lejos de los punteros de función de fundición en algunos casos. En la convención x86 llamar, los argumentos son empujados en la pila, y todos los punteros son del mismo tamaño (4 bytes en x86 o 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 a la funciónobjetivo del puntero, y obviamente hay ninguna noción de tipos a nivel de código máquina.

Las cosas definitivamente no pueden do:

  • moldeada entre los punteros de función de diferentes convenciones de llamada. Usted se hace un lío la pila y en el mejor de accidente, en el peor, a tener éxito en silencio con una enorme enorme agujero de seguridad. En la programación de Windows, que a menudo se pasa alrededor de los punteros de función. Win32 espera que todas las funciones de devolución de llamada para utilizar la convención stdcall llamar (que el CALLBACK macros, PASCAL y WINAPI todo se expanden a). Si pasa un puntero de función que utiliza la convención de llamada C estándar (cdecl), dará lugar a la maldad.
  • En C ++, fundido entre los punteros de función miembro de clase y los punteros de función regulares. Esto muchas veces se enfrenta novatos C ++. funciones miembro de clase tienen un parámetro this escondido, y si lanzas una función miembro de una función regular, no hay objeto this de usar, y otra vez, tanto la maldad resultarán.

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*). punteros de función no son necesariamente del mismo tamaño que los punteros regulares, ya que en algunas arquitecturas que pueden contener información adicional contextual. Esto probablemente funcionará bien en x86, pero recuerde que es un comportamiento indefinido.

Otros consejos

Le pregunté acerca de este problema exactamente el mismo en relación con un cierto código en GLib recientemente. (GLib es una biblioteca de núcleo para el proyecto GNOME y escrita en C.) me dijeron todo el slots'n'signals marco depende de ello.

Durante 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 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;
}

Vea por sí mismo aquí en g_array_sort(): http: //git.gnome .org / Navegar / labia / árbol / labia / garray.c

Las respuestas anteriores son detallados y probablemente correcta - si se siente en el comité de estándares. Adán y Johannes merecen crédito por sus respuestas bien investigados. Sin embargo, en la naturaleza, se encuentra este código funciona muy bien. ¿Polémico? Si. Considere esto: GLib compila / obras / pruebas en un gran número de plataformas (Linux / Solaris / Windows / OS X) con una amplia variedad de compiladores / enlazadores / cargadores del kernel (GCC / CLANG / MSVC). diablo con las normas, supongo.

pasé algún tiempo pensando en estas respuestas. Aquí es mi conclusión:

  1. Si está escribiendo una biblioteca llamada, este podría estar bien. Cuidado con este hotel -. utilizar a su propio riesgo
  2. Si no, no lo haga.

El pensamiento más profundo después de escribir esta respuesta, no me sorprendería que el código para compiladores de C utiliza este mismo truco. Y puesto que (la mayoría / todos?) Modernos compiladores de C se bootstrap, esto implicaría el truco es seguro.

Una cuestión más importante para la investigación: ¿Puede alguien encontrar una plataforma / compilador / enlazador / cargador cuando este truco hace no trabajo? Los principales puntos brownie para que uno. Apuesto a que hay algunos procesadores / sistemas embebidos que no les gusta. Sin embargo, para la computación de escritorio (y, probablemente, móvil / tableta), este truco probablemente todavía funciona.

El punto realmente no es si se puede. 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 usted estaría contento de que lo hizo.

Usted tiene un tipo de función compatibles si el tipo de retorno y parámetros de tipos son compatibles - básicamente (que es más complicado que en realidad :)). La compatibilidad es lo mismo que "mismo tipo" sólo más laxa para permitir disponer de diferentes tipos, pero todavía tienen alguna forma de decir "estos tipos son casi los mismos". En C89, por ejemplo, dos estructuras eran compatibles si fueran de otro modo idéntico, pero sólo su nombre era diferente. C99 parece haber cambiado eso. Citando el c justificación documento (lectura muy recomendable, por cierto!):

  

Estructura declaraciones, unión, o tipo de enumeración en dos diferentes unidades de traducción no se declaran oficialmente del mismo tipo, incluso si el texto de estas declaraciones provienen de la misma archivo de inclusión, ya que las unidades de traducción son ellos mismos disjuntos. Por tanto, la norma especifica reglas de compatibilidad adicionales para tales tipos, de modo que si dos de tales declaraciones son suficientemente similares que 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. Pero, sin embargo, espero que todos los compiladores para compilar y ejecutar sin gemidos. Pero se puede hacer más limpio por tener otra función que toma un void* (y el registro que, como llamada de retorno), que acaba de llamar a su función real a continuación.

A medida que compila el código C a la instrucción que no le importa en absoluto acerca de los tipos de puntero, que es bastante malo usar el código que mencionas. Te llegas a tener problemas cuando había corrido do_stuff con su función de devolución de llamada y el puntero a otra cosa a continuación, la estructura my_struct 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 siguen teniendo sentido en tiempo de ejecución.

Si se piensa en la forma en función de llamadas de trabajo en C / C ++, empujan ciertos elementos en la pila, el salto a la nueva ubicación del código, ejecutar, a continuación, sacar la pila en la declaración. Si los punteros de función describen las funciones con el mismo tipo de retorno y el mismo número / tamaño de argumentos, usted debe estar bien.

Por lo tanto, creo que debería ser capaz de hacerlo con seguridad.

punteros void son compatibles con otros tipos de puntero. Es la columna vertebral de la forma (memcpy, memcmp) malloc trabajo y las funciones mem. Típicamente, en C (En lugar de C ++) NULL es una macro definida como ((void *)0).

Look at 6.3.2.3 (punto 1) en C99:

  

Un puntero para anular se puede convertir en o desde un puntero a cualquier tipo incompleto o el objeto

Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top