views:

1035

answers:

3

I have always been a bit stumped when I read other peoples' code which had typedefs for pointers to functions with arguments. I recall that it took me a while to get around to such a definition while trying to understand a numerical algorithm written in C a while ago. So, could you share your tips and thoughts on how to write good typedefs for pointers to functions (Do's and Do not's), as to why are they useful and how to understand others' work? Thanks!

+3  A: 

cdecl is a great tool for deciphering weird syntax like function pointer declarations. You can use it to generate them as well.

As far as tips for making complicated declarations easier to parse for future maintenance (by yourself or others), I recommend making typedefs of small chunks and using those small pieces as building blocks for larger and more complicated expressions. For example:

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

rather than:

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

cdecl can help you out with this stuff:

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 )

And is (in fact) exactly how I generated that crazy mess above.

Carl Norum
Hi Carl, that was a very insightful example and explanation. Also, thanks for showing the using of cdecl. Much appreciated.
Sam
+3  A: 

A function pointer is like any other pointer, but it points to the address of a function instead of the address of data (on heap or stack). Like any pointer, it needs to be typed correctly. Functions are defined by their return value and the types of parameters they accept. So in order to fully describe a function, you must include its return value and the type of each parameter is accepts. When you typedef such a definition, you give it a 'friendly name' which makes it easier to create and reference pointers using that definition.

So for example assume you have a function:

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

then the following typedef:

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

can be used to point to this doMulitplication function. It is simply defining a pointer to a function which returns a float and takes two parameters, each of type float. This definition has the friendly name pt2Func. Note that pt2Func can point to ANY function which returns a float and takes in 2 floats.

So you can create a pointer which points to the doMultiplication function as follows:

pt2Func *myFnPtr = &doMultiplication;

and you can invoke the function using this pointer as follows:

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

This makes good reading: http://www.newty.de/fpt/index.html

psychotik
psychotik, thanks! That was helpful. The link to the function pointers web page is really helpful. Reading it now.
Sam
... However, that newty.de link does not appear to talk about typedefs at all :(So even though that link is great, but the responses in this thread about typedefs are invaluable!
Sam
+5  A: 

Consider the 'signal()` function from the C standard:

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

Perfectly obscurely obvious - it's a function that takes two arguments, an integer and a pointer to a function that takes an integer as an argument and returns nothing, and it (signal()) returns a pointer to a function that takes an integer as an argument and returns nothing.

If you write:

typedef void (*SignalHandler)(int signum);

then you can instead declare 'signal()' as:

extern  SignalHandler signal(int signum, SignalHandler handler);

This means the same thing, but is usually regarded as somewhat easier to read. It is clearer that the function takes an int and a SignalHandler and returns a SignalHandler.

It takes a bit of getting used to, though. The one thing you can't do, though is write a signal handler function using the SignalHandler typedef in the function definition.

I'm still of the old-school that prefers to invoke a function pointer as:

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

Modern syntax uses just:

functionpointer(arg1, arg2, ...);

I can see why that works - I just prefer to know that I need to look for where the variable is initialized rather than for a function called 'functionpointer'.


Sam commented:

I have seen this explanation before. And then, as is the case now, I think what I didn't get was the connection between the two statements:

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

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

Or, what I want to ask is, what is the underlying concept that one can use to come up with the second version you have? What is the fundamental that connects "SignalHandler" and the first typedef? I think what needs to be explained here is what is typedef is actually doing here.

Let's try again. The first of these is lifted straight from the C standard - I retyped it, and checked that I had the parentheses right (not until I corrected it - it is a tough cookie to remember).

First of all, remember that 'typedef' introduces an alias for a type. So, the alias is 'SignalHandler', and its type is:

a pointer to a function that takes an integer as an argument and returns nothing.

The 'returns nothing' part is spelled 'void'; the argument that is an integer is (I trust) self-explanatory. The following notation is simply (or not) how C spells pointer to function taking arguments as specified and returning the given type:

type (*function)(argtypes);

After creating the signal handler type, I can use it to declare variables and so on. For example:

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

So, what have we done here - apart from omit 4 standard headers that would be needed to make the code compile cleanly?

The first two functions are functions that take a single integer and return nothing. One of them actually doesn't return at all thanks to the 'exit(1);' but the other does return after printing a message. Be aware that the C standard does not permit you to do very much inside a signal handler; POSIX is a bit more generous in what is allowed, but officially does not sanction calling 'fprintf()'. I also print out the signal number that was received. In the 'alarm_handler()' function, the value will always be SIGALRM as that is the only signal that it is a handler for, but 'signal_handler()' might get SIGINT or SIGQUIT as the signal number because the same function is used for both.

Then I create an array of structures, where each element identifies a signal number and the handler to be installed for that signal. I've chosen to worry about 3 signals; I'd often worry about SIGHUP, SIGPIPE and SIGTERM too and about whether they are defined ('#ifdef' conditional compilation), but that just complicates things. I'd also probably use POSIX 'sigaction()' instead of 'signal()', but that is another issue; let's stick with what we started with.

The 'main()' function iterates over the list of handlers to be installed. For each handler, it first calls 'signal()' to find out whether the process is currently ignoring the signal, and while doing so, installs SIG_IGN as the handler, which ensures that the signal stays ignored. If the signal was not previously being ignored, it then calls 'signal()' again, this time to install the preferred signal handler. (The other value is presumably SIG_DFL, the default signal handler for the signal.) Because the first call to 'signal()' set the handler to SIG_IGN and 'signal()' returns the previous error handler, the value of 'old' after the 'if' statement must be SIG_IGN - hence the assertion. (Well, it could be SIG_ERR if something went dramatically wrong - but then I'd learn about that from the assert firing.)

The program then does its stuff and exits normally.

Note that the name of a function can be regarded as a pointer to a function of the appropriate type. When you do not apply the function-call parentheses - as in the initializers, for example - the function name becomes a function pointer. This is also why it is reasonable to invoke functions via the 'pointertofunction(arg1, arg2)' notation; when you see 'alarm_handler(1)', you can consider that 'alarm_handler' is a pointer to the function and therefore 'alarm_handler(1)' is an invocation of a function via a function pointer.

So, thus far, I've shown that a SignalHandler variable is relatively straight-forward to use, as long as you have some of the right type of value to assign to it - which is what the two signal handler functions provide.

Now we get back to the question - how do the two declarations for 'signal()' relate to each other.

Let's review the second declaration:

 extern SignalHandler signal(int signum, SignalHandler handler);

If we changed the function name and the type like this:

 extern double function(int num1, double num2);

you would have no problem interpreting this as a function that takes an int and a double as arguments and returns a double value (would you? maybe you'd better not 'fess up if that is problematic - but maybe you should be cautious about asking questions as hard as this one if it is a problem).

Now, instead of being a double, the 'signal()' function takes a SignalHandler as its second argument, and it returns one as its result.

The mechanics by which that can also be treated as:

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

are tricky to explain - so I'll probably screw it up. This time I've given the parameters names - though the names aren't critical.

In general, in C, the declaration mechanism is such that if you write:

type var;

then when you write 'var' it represents a value of the given type. For example:

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

In the standard, 'typedef' is treated as a storage class in the grammar, rather like 'static' and 'extern' are storage classes.

typedef void (*SignalHandler)(int signum);

means that when you see a variable of type SignalHandler (say alarm_handler) invoked as:

(*alarm_handler)(-1);

the result has 'type void' - there is no result. And (*alarm_handler)(-1); is an invocation of alarm_handler() with argument -1.

So, if we declared:

extern SignalHandler alt_signal(void);

it means that:

(*alt_signal)();

represents a void value. And therefore:

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

is equivalent. Now, 'signal()' is more complex because it not only returns a SignalHandler, it also accepts both an int and a SignalHandler as arguments:

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

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

If that still confuses you, I'm not sure how to help - it is still at some levels mysterious to me, but I've grown used to how it works and can therefore tell you that if you stick with it for another 25 years or so, it will become second nature to you (and maybe even a bit quicker if you are clever).

Jonathan Leffler
I thought I knew how to read pretty much any C code but this signal function gave me trouble!Good reply.
toto
I have seen this explanation before. And then, as is the case now, I think what I did't get was the connection between the two statements: extern void (*signal(int, void(*)(int)))(int);/*and*/ typedef void (*SignalHandler)(int signum); extern SignalHandler signal(int signum, SignalHandler handler);Or, what I want to ask is, what is the underlying concept that one can use to come up with the second version you have? What is the fundamental that connects "SignalHandler" and the first typedef?I think what needs to be explained here is what is typedef is actually doing here.Thx
Sam
http://en.wikipedia.org/wiki/Typedef
psychotik
@psychotik: the Wikipedia article mentions typedefs and pointers to functions, but does not illustrate them at all. Maybe I should put some of this answer into the Wikipedia article :D
Jonathan Leffler
Hi Jonathan, great reply!I use C and C++ to implement mathematical algorithm usually but never for programming an OS or other low level system related stuff. If this were known, perhaps it would have saved some typing on your part. The meat of your reply, for me, was when you started to clarify the "define" as a storage class in the grammar.I am familiar only with the usual typedefs. Even though I am familiar with ptrs to funcs, my problem was with the exact syntax of typedefs for func ptrs.You summarized well in your last sentences. The syntax needs some getting used to.Regards.
Sam
Great answer, I'm glad I came back to this thread. I don't think I understand everything, but one day I will.This is why I like SO. Thank you.
toto