Comprender los typedefs para los punteros de función en C

237

Siempre me he quedado un poco perplejo cuando leía el código de otras personas que tenía typedefs para punteros a funciones con argumentos. Recuerdo que me tomó un tiempo llegar a esa definición al tratar de entender un algoritmo numérico escrito en C hace un tiempo. Entonces, ¿podría compartir sus consejos y reflexiones sobre cómo escribir buenos typedefs para punteros a funciones (Do's y Do not's), en cuanto a por qué son útiles y cómo entender el trabajo de los demás? ¡Gracias!

nbro
fuente
1
¿Puedes dar algunos ejemplos?
Artelius el
2
¿No quiere decir typedefs para punteros de función, en lugar de macros para punteros de función? He visto lo primero pero no lo segundo.
dave4420

Respuestas:

297

Considere la signal()función del estándar C:

extern void (*signal(int, void(*)(int)))(int);

Perfectamente oscuro y obvio: es una función que toma dos argumentos, un entero y un puntero a una función que toma un entero como argumento y no devuelve nada, y ( signal()) devuelve un puntero a una función que toma un entero como argumento y devuelve nada.

Si tú escribes:

typedef void (*SignalHandler)(int signum);

entonces puedes declarar signal()como:

extern  SignalHandler signal(int signum, SignalHandler handler);

Esto significa lo mismo, pero generalmente se considera algo más fácil de leer. Es más claro que la función toma a inty a SignalHandlery devuelve a SignalHandler.

Sin embargo, lleva un poco acostumbrarse. Sin embargo, lo único que no puede hacer es escribir una función de controlador de señal utilizando SignalHandler typedefen la definición de la función.

Todavía soy de la vieja escuela que prefiere invocar un puntero de función como:

(*functionpointer)(arg1, arg2, ...);

La sintaxis moderna usa solo:

functionpointer(arg1, arg2, ...);

Puedo ver por qué eso funciona: prefiero saber que necesito buscar dónde se inicializa la variable en lugar de una función llamada functionpointer.


Sam comentó:

He visto esta explicación antes. Y luego, como es el caso ahora, creo que lo que no entendí fue la conexión entre las dos declaraciones:

    extern void (*signal(int, void()(int)))(int);  /*and*/

    typedef void (*SignalHandler)(int signum);
    extern SignalHandler signal(int signum, SignalHandler handler);

O, lo que quiero preguntar es, ¿cuál es el concepto subyacente que uno puede usar para llegar a la segunda versión que tiene? ¿Cuál es el fundamento que conecta "SignalHandler" y el primer typedef? Creo que lo que hay que explicar aquí es lo que typedef está haciendo realmente aquí.

Intentemoslo de nuevo. El primero de ellos se levanta directamente del estándar C: lo volví a escribir y verifiqué que tenía los paréntesis correctos (no hasta que lo corrija, es una galleta difícil de recordar).

En primer lugar, recuerde que typedefintroduce un alias para un tipo. Entonces, el alias es SignalHandler, y su tipo es:

un puntero a una función que toma un entero como argumento y no devuelve nada.

La parte 'no devuelve nada' se deletrea void; El argumento que es un entero es (confío) autoexplicativo. La siguiente notación es simplemente (o no) cómo C deletrea el puntero para funcionar tomando argumentos como se especifica y devolviendo el tipo dado:

type (*function)(argtypes);

Después de crear el tipo de controlador de señal, puedo usarlo para declarar variables, etc. Por ejemplo:

static void alarm_catcher(int signum)
{
    fprintf(stderr, "%s() called (%d)\n", __func__, signum);
}

static void signal_catcher(int signum)
{
    fprintf(stderr, "%s() called (%d) - exiting\n", __func__, signum);
    exit(1);
}

static struct Handlers
{
    int              signum;
    SignalHandler    handler;
} handler[] =
{
    { SIGALRM,   alarm_catcher  },
    { SIGINT,    signal_catcher },
    { SIGQUIT,   signal_catcher },
};

int main(void)
{
    size_t num_handlers = sizeof(handler) / sizeof(handler[0]);
    size_t i;

    for (i = 0; i < num_handlers; i++)
    {
        SignalHandler old_handler = signal(handler[i].signum, SIG_IGN);
        if (old_handler != SIG_IGN)
            old_handler = signal(handler[i].signum, handler[i].handler);
        assert(old_handler == SIG_IGN);
    }

    ...continue with ordinary processing...

    return(EXIT_SUCCESS);
}

Tenga en cuenta ¿Cómo evitar usar printf()en un controlador de señal?

Entonces, ¿qué hemos hecho aquí, aparte de omitir 4 encabezados estándar que serían necesarios para que el código se compile limpiamente?

Las dos primeras funciones son funciones que toman un solo entero y no devuelven nada. Uno de ellos no regresa en absoluto gracias al, exit(1);pero el otro sí regresa después de imprimir un mensaje. Tenga en cuenta que el estándar C no le permite hacer mucho dentro de un controlador de señal; POSIX es un poco más generoso en lo que está permitido, pero oficialmente no sanciona las llamadas fprintf(). También imprimo el número de señal que se recibió. En la alarm_handler()función, el valor siempre será, SIGALRMya que es la única señal para la que es un manejador, pero signal_handler()podría obtener SIGINTo SIGQUITcomo el número de señal porque se usa la misma función para ambos.

Luego creo una matriz de estructuras, donde cada elemento identifica un número de señal y el controlador que se instalará para esa señal. Elegí preocuparme por 3 señales; A menudo me preocupa SIGHUP, SIGPIPEy SIGTERMtambién y si están definidos ( #ifdefcompilación condicional), pero eso complica las cosas. Probablemente también usaría POSIX en sigaction()lugar de signal(), pero ese es otro problema; sigamos con lo que comenzamos.

La main()función itera sobre la lista de controladores que se instalarán. Para cada controlador, primero llama signal()para averiguar si el proceso ignora actualmente la señal y, al hacerlo, se instala SIG_IGNcomo el controlador, lo que garantiza que la señal permanezca ignorada. Si la señal no se ignoraba anteriormente, vuelve a llamar signal(), esta vez para instalar el controlador de señal preferido. (Presumiblemente SIG_DFL, el otro valor es el controlador de señal predeterminado para la señal.) Debido a que la primera llamada a 'signal ()' establece el controlador SIG_IGNy signal()devuelve el controlador de error anterior, el valor de olddespués de la ifdeclaración debe ser SIG_IGN, de ahí la afirmación. (Bueno, podría serSIG_ERR si algo salió dramáticamente mal, pero luego me enteraría de eso por el disparo afirmativo).

El programa luego hace sus cosas y sale normalmente.

Tenga en cuenta que el nombre de una función puede considerarse como un puntero a una función del tipo apropiado. Cuando no aplica los paréntesis de llamadas a funciones, como en los inicializadores, por ejemplo, el nombre de la función se convierte en un puntero de función. Por eso también es razonable invocar funciones a través de la pointertofunction(arg1, arg2)notación; cuando vea alarm_handler(1), puede considerar que alarm_handleres un puntero a la función y, por alarm_handler(1)lo tanto, es una invocación de una función a través de un puntero de función.

Entonces, hasta ahora, he demostrado que una SignalHandlervariable es relativamente sencilla de usar, siempre que tenga el tipo de valor correcto para asignarle, que es lo que proporcionan las dos funciones de controlador de señal.

Ahora volvemos a la pregunta: ¿cómo se relacionan entre sí las dos declaraciones signal()?

Repasemos la segunda declaración:

 extern SignalHandler signal(int signum, SignalHandler handler);

Si cambiamos el nombre de la función y el tipo de esta manera:

 extern double function(int num1, double num2);

usted no tendría ningún problema interpretar esto como una función que toma un inty una doublecomo argumentos y devuelve un doublevalor (que sería tal vez mejor que no 'confesarse, si eso es problemático? -, pero tal vez debería tener cuidado de no hacer preguntas, según duro como este si es un problema).

Ahora, en lugar de ser un double, la signal()función toma a SignalHandlercomo su segundo argumento, y devuelve uno como resultado.

La mecánica por la cual eso también se puede tratar como:

extern void (*signal(int signum, void(*handler)(int signum)))(int signum);

son difíciles de explicar, así que probablemente lo arruine. Esta vez he dado los nombres de los parámetros, aunque los nombres no son críticos.

En general, en C, el mecanismo de declaración es tal que si escribe:

type var;

entonces cuando lo escribes varrepresenta un valor de lo dado type. Por ejemplo:

int     i;            // i is an int
int    *ip;           // *ip is an int, so ip is a pointer to an integer
int     abs(int val); // abs(-1) is an int, so abs is a (pointer to a)
                      // function returning an int and taking an int argument

En el estándar, typedefse trata como una clase de almacenamiento en la gramática, más bien como staticy externson clases de almacenamiento.

typedef void (*SignalHandler)(int signum);

significa que cuando ve una variable de tipo SignalHandler(digamos alarm_handler) invocada como:

(*alarm_handler)(-1);

el resultado tiene type void- no hay resultado. Y (*alarm_handler)(-1);es una invocación de alarm_handler()con argumento -1.

Entonces, si declaramos:

extern SignalHandler alt_signal(void);

esto significa que:

(*alt_signal)();

representa un valor nulo. Y por lo tanto:

extern void (*alt_signal(void))(int signum);

es equivalente. Ahora, signal()es más complejo porque no solo devuelve a SignalHandler, también acepta tanto un int como un SignalHandlerargumento:

extern void (*signal(int signum, SignalHandler handler))(int signum);

extern void (*signal(int signum, void (*handler)(int signum)))(int signum);

Si eso todavía lo confunde, no estoy seguro de cómo ayudarlo, todavía es misterioso en algunos niveles, pero me he acostumbrado a cómo funciona y, por lo tanto, puedo decirle que si lo sigue por otros 25 años más o menos, se convertirá en una segunda naturaleza para usted (y tal vez incluso un poco más rápido si es inteligente).

Jonathan Leffler
fuente
3
He visto esta explicación antes. Y luego, como es el caso ahora, creo que lo que no obtuve fue la conexión entre las dos declaraciones: extern void ( signal (int, void ( ) (int))) (int); / * y * / typedef void (* SignalHandler) (int signum); señal externa SignalHandler (int signum, controlador SignalHandler); O, lo que quiero preguntar es, ¿cuál es el concepto subyacente que uno puede usar para llegar a la segunda versión que tiene? ¿Cuál es el fundamento que conecta "SignalHandler" y el primer typedef? Creo que lo que hay que explicar aquí es lo que typedef está haciendo realmente aquí. Thx
66
Gran respuesta, me alegro de haber vuelto a este hilo. No creo que entienda todo, pero algún día lo haré. Por eso me gusta SO. Gracias.
toto
2
Solo para elegir: no es seguro llamar a printf () y amigos dentro de un controlador de señal; printf () no es reentrante (básicamente porque puede llamar a malloc (), que no es reentrante)
wildplasser
44
Esto extern void (*signal(int, void(*)(int)))(int);significa que la signal(int, void(*)(int))función devolverá un puntero de función a void f(int). Cuando desee especificar un puntero de función como valor de retorno , la sintaxis se complica. Debe colocar el tipo de valor de retorno a la izquierda y la lista de argumentos a la derecha , mientras que es el medio que está definiendo. Y en este caso, la signal()función en sí misma toma un puntero de función como parámetro, lo que complica aún más las cosas. La buena noticia es que si puedes leer esta, la Fuerza ya está contigo. :).
smwikipedia
1
¿Qué es la vieja escuela sobre usar &frente al nombre de una función? Es totalmente innecesario; sin sentido, incluso. Y definitivamente no es "vieja escuela". La vieja escuela usa un nombre de función simple y llanamente.
Jonathan Leffler
80

Un puntero de función es como cualquier otro puntero, pero apunta a la dirección de una función en lugar de la dirección de datos (en el montón o pila). Como cualquier puntero, debe escribirse correctamente. Las funciones se definen por su valor de retorno y los tipos de parámetros que aceptan. Entonces, para describir completamente una función, debe incluir su valor de retorno y se acepta el tipo de cada parámetro. Cuando escribe def una definición, le da un 'nombre descriptivo' que facilita la creación y referencia de punteros utilizando esa definición.

Entonces, por ejemplo, suponga que tiene una función:

float doMultiplication (float num1, float num2 ) {
    return num1 * num2; }

entonces el siguiente typedef:

typedef float(*pt2Func)(float, float);

se puede usar para señalar esta doMulitplicationfunción. Simplemente está definiendo un puntero a una función que devuelve un flotante y toma dos parámetros, cada uno de tipo flotante. Esta definición tiene el nombre descriptivo pt2Func. Tenga en cuenta que pt2Funcpuede apuntar a CUALQUIER función que devuelva un flotador y tome 2 flotadores.

Por lo tanto, puede crear un puntero que apunte a la función doMultiplication de la siguiente manera:

pt2Func *myFnPtr = &doMultiplication;

y puede invocar la función usando este puntero de la siguiente manera:

float result = (*myFnPtr)(2.0, 5.1);

Esto es una buena lectura: http://www.newty.de/fpt/index.html

psicotik
fuente
Psychotik, gracias! Eso fue útil. El enlace a la página web de punteros de función es realmente útil. Leyéndolo ahora.
... Sin embargo, ese enlace newty.de no parece hablar sobre typedefs en absoluto :( Entonces, aunque ese enlace es excelente, ¡pero las respuestas en este hilo sobre typedefs son invaluables!
11
Es posible que desee hacer en pt2Func myFnPtr = &doMultiplication;lugar de pt2Func *myFnPtr = &doMultiplication;como myFnPtrya es un puntero.
Tamilselvan
1
declarando pt2Func * myFnPtr = & doMultiplication; en lugar de pt2Func myFnPtr = & doMultiplication; lanza una advertencia
AlphaGoku
2
@Tamilselvan es correcto. myFunPtrya es un puntero de función así que el usopt2Func myFnPtr = &doMultiplication;
Dustin Biser
35

Una manera muy fácil de entender typedef del puntero de función:

int add(int a, int b)
{
    return (a+b);
}

typedef int (*add_integer)(int, int); //declaration of function pointer

int main()
{
    add_integer addition = add; //typedef assigns a new variable i.e. "addition" to original function "add"
    int c = addition(11, 11);   //calling function via new variable
    printf("%d",c);
    return 0;
}
usuario2786027
fuente
32

cdecles una gran herramienta para descifrar sintaxis extraña como declaraciones de puntero de función. Puedes usarlo para generarlos también.

En cuanto a los consejos para hacer que las declaraciones complicadas sean más fáciles de analizar para el mantenimiento futuro (usted mismo u otros), recomiendo hacer typedefpequeños fragmentos y usar esas piezas pequeñas como bloques de construcción para expresiones más grandes y más complicadas. Por ejemplo:

typedef int (*FUNC_TYPE_1)(void);
typedef double (*FUNC_TYPE_2)(void);
typedef FUNC_TYPE_1 (*FUNC_TYPE_3)(FUNC_TYPE_2);

más bien que:

typedef int (*(*FUNC_TYPE_3)(double (*)(void)))(void);

cdecl puede ayudarte con estas cosas:

cdecl> explain int (*FUNC_TYPE_1)(void)
declare FUNC_TYPE_1 as pointer to function (void) returning int
cdecl> explain double (*FUNC_TYPE_2)(void)
declare FUNC_TYPE_2 as pointer to function (void) returning double
cdecl> declare FUNC_TYPE_3 as pointer to function (pointer to function (void) returning double) returning pointer to function (void) returning int
int (*(*FUNC_TYPE_3)(double (*)(void )))(void )

Y es (de hecho) exactamente cómo generé ese loco desastre anterior.

Carl Norum
fuente
2
Hola Carl, ese fue un ejemplo y una explicación muy perspicaz. Además, gracias por mostrar el uso de cdecl. Muy apreciado.
¿Hay cdecl para windows?
Jack
@ Jack, estoy seguro de que puedes construirlo, sí.
Carl Norum
2
También hay cdecl.org que proporciona el mismo tipo de capacidad pero en línea. Útil para nosotros los desarrolladores de Windows.
zaknotzach
12
int add(int a, int b)
{
  return (a+b);
}
int minus(int a, int b)
{
  return (a-b);
}

typedef int (*math_func)(int, int); //declaration of function pointer

int main()
{
  math_func addition = add;  //typedef assigns a new variable i.e. "addition" to original function "add"
  math_func substract = minus; //typedef assigns a new variable i.e. "substract" to original function "minus"

  int c = addition(11, 11);   //calling function via new variable
  printf("%d\n",c);
  c = substract(11, 5);   //calling function via new variable
  printf("%d",c);
  return 0;
}

La salida de esto es:

22

6 6

Tenga en cuenta que se ha utilizado el mismo definidor math_func para declarar tanto la función.

Se puede usar el mismo enfoque de typedef para la estructura externa (usando sturuct en otro archivo).

Harshal Doshi Jain
fuente
5

Utilice typedefs para definir tipos más complicados, es decir, punteros de función

Tomaré el ejemplo de definir una máquina de estado en C

    typedef  int (*action_handler_t)(void *ctx, void *data);

ahora hemos definido un tipo llamado action_handler que toma dos punteros y devuelve un int

define tu máquina de estado

    typedef struct
    {
      state_t curr_state;   /* Enum for the Current state */
      event_t event;  /* Enum for the event */
      state_t next_state;   /* Enum for the next state */
      action_handler_t event_handler; /* Function-pointer to the action */

     }state_element;

El puntero de función a la acción parece un tipo simple y typedef sirve principalmente para este propósito.

Todos mis controladores de eventos ahora deben cumplir con el tipo definido por action_handler

    int handle_event_a(void *fsm_ctx, void *in_msg );

    int handle_event_b(void *fsm_ctx, void *in_msg );

Referencias

Programación experta en C por Linden

vaaz
fuente
4

Este es el ejemplo más simple de punteros de función y matrices de puntero de función que escribí como ejercicio.

    typedef double (*pf)(double x);  /*this defines a type pf */

    double f1(double x) { return(x+x);}
    double f2(double x) { return(x*x);}

    pf pa[] = {f1, f2};


    main()
    {
        pf p;

        p = pa[0];
        printf("%f\n", p(3.0));
        p = pa[1];
        printf("%f\n", p(3.0));
    }
Bing Bang
fuente