The Missing Parentheses That Turned My Callback Into Something Else
I wrote a function-pointer parameter, minus two parentheses. It compiled without a single complaint — because what I'd actually written was still a pointer, just one aimed at a function with the wrong return type.
I wrote a function signature that was supposed to take a callback, looked at it for a couple seconds, decided it was fine, and moved on. It compiled with zero warnings. Turns out that didn't mean much — what I'd actually typed was still a function-pointer parameter, just one pointing at a function with the wrong return type. It was legal C, close enough to what I meant that I didn't catch the difference.
The setup
I was messing around with a small dispatcher — a function that takes two ints plus something telling it how to combine them, so I could pass in add or sub instead of writing five near-identical functions. I think this pattern shows up again later on, interrupt handlers or dispatch tables or something like that, not totally sure yet, but that's part of why I wanted to get comfortable with it now instead of later. I wrote:
int func(int a, int b, int *p(int, int));In my head, p was a pointer to a function taking two ints and returning an int, and func would call p(a, b) with whatever got handed to it.
What went wrong
Turns out p doesn't mean what I thought, but it's subtler than "not a pointer." () right after an identifier binds to that identifier before * does, so *p(int, int) reads as "p is a function of (int, int) returning int *" — the int * out front describes what that function returns, not what p itself is. Sitting on its own, that really would just be a plain function declaration, no pointer anywhere.
But p here isn't sitting on its own — it's a parameter of func. And a parameter is never actually allowed to have function type, the same way it's never allowed to have array type: the compiler silently adjusts it to a pointer, no (*p) required, exactly like an array parameter silently becomes a pointer to its element type. So what I'd actually declared was already a pointer to a function going into func — just a pointer to a function that returns int *, when what I meant was a pointer to a function that returns plain int. The parentheses I skipped were never deciding "pointer or not." They were deciding which function the pointer points to.
That's also why none of this throws an error at the declaration itself. It's legal C, just not the C I meant — a function-pointer parameter with one unwanted extra level of indirection baked into its return type, not a totally different kind of thing. The place it would've actually mattered is a call site: passing add or sub, both returning plain int, into that slot would've triggered an incompatible-pointer-type warning over the return type, not a "that's not a pointer" error. I didn't work out the parameter-adjustment part on my own — someone had to walk me through it — and I went back and stared at the line for a solid minute afterward, trying to actually see it myself instead of just being told it.
The fix
int func(int a, int b, int (*p)(int, int));Putting parens around *p forces the grammar to group those first — p is declared a pointer directly, and then "function of (int, int) returning int" describes what it points to, with no extra int * layered on top of the return type. The parameter was always going to end up a pointer either way; the parens are what fix what it's a pointer to. I've started trying to read these right to left, name first: p is a pointer, to a function taking (int, int), returning int. Whether that habit actually sticks past this week, I don't know yet.
A few days later there was a trace-through question testing a related but genuinely different case: given int *fp2(int, int); fp2 = add;, what is fp2? This one really is just a function — fp2 here isn't a parameter, so none of the pointer-adjustment from the func example applies, and I got it right: function declaration, not a pointer variable, which meant the next line couldn't compile at all, since you can't assign to a function. That felt like a better sign than getting the first one right, and it's the pairing I should've had from the start: the exact same *name(params) shape is a plain function outside a parameter list, and an automatically-adjusted function pointer inside one.
I think the rule is that function pointer syntax is always return_type (*name)(param_types), and the parens around *name are doing actual work, not just style — leave them out in a plain declaration and you get a function instead of a pointer; leave them out in a parameter list and you still get a pointer, just one aimed at the wrong return type, which is arguably the sneakier failure since nothing about it looks broken. I'm going to try to actually check for this now, any time I write something mixing * and (): read it right to left starting at the name, and separately ask whether I'm even inside a parameter list, since that changes what leaving the parens off actually costs me. Whether I'll remember to do that reliably or just get caught by this exact thing again in three weeks, no idea. Past experience says it's probably not a one-and-done fix. I've thought a rule was "locked in" before and then watched myself do the exact same thing a week later on a slightly different line, so I'm trying not to feel too confident about this one just because I can explain it right now.
Notes from readers
Comments — via GitHub