Making Function Pointers Usable In C
If you say the phrase "function pointer," does it make you cringe? Does it make your coworkers break out in a cold sweat?
We've seen the function pointer coding disasters. We've heard the horror stories about code jumping into random spots of memory and trying to execute it as if it were our function.
And there is the notation. Egad! The notation for function pointers in C is truly horrific, isn't it? After many years, I still occasionally need to look up what the proper syntax is for a function pointer typedef (Clearly, I'm not a C compiler).
But there are just certain situations where a well-placed function pointer does the job like no other solution. A few of my favorite examples:
Dispatch Tables - Your function has received an ID. It might be processing incoming messages. It might be handling image processing. It could be many things... but the point is that you have a set of operations that need to be called, based on a single key or index of some type. If those operations are called in a similar way, you've got the perfect place for an array of function pointers.
Pipelines - You want to chain data from one step to another. Maybe some of the steps are optional. Maybe you intend to just run through all of them. The output of one step becomes the input to the next. Again, you've likely found the perfect place for a set of function pointers.
Callbacks - I actually avoid callbacks for the most part... but there are certain situations where a callback can handle things more cleanly than any other solution. You know what callbacks are? Yes. Function pointers.
OK. So we've established that function pointers sometimes have their place... but what can we do to keep ourselves out of trouble? I'm so glad that you asked! I just happen to have a few tricks that I have learned that I would love to share. If you've learned tricks of your own, I'd love to hear them!
OH, SYNTAX
Okay, so it's a given that we're going to have to face the dreaded function pointer syntax at SOME point, but at least we can keep it from rearing it's ugly head everywhere we turn! We can look it up once, and then forget about it again; We get some type safety thrown in for free (particularly if you have all your warnings turned on); You even get more readable code. What's not to love?
So don't define your function pointer directly.
int16_t (*myFunc)(int8_t);
Don't (this is even worse) put it directly in your argument list.
int16_t OuterFunc(int32_t val, int16_t (*innerFunc)(int8_t));
And please don't (Oh! The Humanity!) hardcode it into your struct-based table.
const struct DispatchTable {
const char* key;
const int16_t (*func)(int8_t);
}[] = { /****/ };
Finally, don't (Shudder!) return a function pointer directly from a function without a typedef:
int16_t (*CalledFunc(int32_t val))(int8_t);
Isn't it nicer just to create a typedef and then use it for all these other needs? I mean, this looks like a human being can actually understand it, right?
typedef int16_t (*FUNC_T)(int8_t);
typedef struct _DISPATCH_T {
const char* key;
const FUNC_T func;
} DISPATCH_T;
FUNC_T myFunc;
int16_t OuterFunc(int32_t val, FUNC_T innerFunc);
const DISPATCH_T DispatchTable[] = { /****/ };
FUNC_T CalledFunc(int32_t val);
Use Typedefs. Your future self will thank you.
BONUS
Why does the DISPATCH_T typedef also have the struct name _DISPATCH_T in it? Excellent question! There are numerous embedded compilers out there that won't put your typedef names into their debug output. It's unfortunate, because that would help the readability of their output, but I can understand how it happens. They've already stripped that layer of information away before it gets to that error. ANYWAY, if you put your struct name in, you'll often get error messages that make more sense. Just a tip.
DANGER!
So, we've made our function pointers more readable with our handy typedef... but aren't they still kinda... dangerous?
Well yes, they can be. Luckily there is something we can do to reduce that risk... actually multiple somethings. Let's check them out.
CONST
Yes, the first one is our friend const. By making our function pointers const whenever possible, we are reducing the risk that it is going to get clobbered by something else going on in our system. This is particularly true on systems that are ROM-able (or at least put their const data in Flash).
This leads to another accidental bonus of our typedef usage. It's clear that this will create a const function pointer which points at ExternalFunc.
const FUNC_T myFunc = ExternalFunc;
Can you remember the syntax for doing that longhand? I can usually get it by the second try, but really... shouldn't we stick with code that we can get right always? For the record, it's this:
int16_t (const *myFunc)(int8_t);
Don't forget that if you pass a function pointer, the function accepting it should usually have that pointer declared as const too. It's unlikely that any pointer manipulation is going to be desirable... you're usually just going to call that function at some point. Might as well save the compiler some work and assure it that you're leaving it unchanged.
NULL AND BOUNDS CHECKING
The other huge thing you can do is good validation. When working with function pointers, you want to be very sure that the pointer you're working with is valid. These rules are true of any pointers, I know... but consider them extra strict when function pointers are in play.
First, if you're passing function pointers around, EVERY function that accepts a function pointer should validate that it's not NULL. For this to be effective, make sure that you ALWAYS declare your function pointer variables to be either a valid function or NULL. Letting the compiler handle it for you should not be considered adequate. Be explicit.
Second, if you have a const table somewhere, clearly the function pointers aren't going to change, right? But we can still get into trouble! Here is where we want to do some serious bounds checking. If the index or key or whatever isn't in your table, we want to know about it!
This is such a horrible place for an off-by-one error! In fact, I've seen a really handy practice that I've started to make use of myself: In arrays where your key is the index into it, the last element of the array is always an error handler. In that case, if there is an off by one error, it will at least be executing code meant for handling errors already!
If there are any "holes" in your table, clearly we will want to replace these with some sort of default handler as well. It might be a do-nothing function or an error function (depending on the situation). We just want to make sure it is handled.
POINT ON!
There we go. That's the tips I've accumulated for using function pointers in C. I hope this will help others to not fear our flexible little function pointing friends, but that it will also help breed a healthy caution around them. Watch out, they bite!
A SECOND BONUS!
As an added bonus, I thought I would mention that CException works particularly well in situations where we are using Function pointers for dispatch or series (pipelines). In both cases, the calling function can wrap our calls in a Try... Catch block, which means all our handlers can just throw errors when they run into them. Clearly, though, this is a topic for another day.