Why C's Function Pointer Syntax is a Nightmare (And How to Survive It)
Problem
When I looked at this C function declaration for the first time, I froze:
void (*signal(int sig, void (*handler)(int)))(int);I tried to parse it mentally. I thought signal returns a void. But then I saw (*signal(...)). So maybe it’s a pointer? But to what? And what’s with the nested parentheses?
I stared at it for 5 minutes. I couldn’t make sense of it. This is the standard Unix signal handler function, something every C programmer should understand. But the syntax looked like alphabet soup.
What happened?
I was learning C programming and wanted to understand signal handling. I found this declaration in <signal.h>. I tried to write a simple program:
#include <signal.h>#include <stdio.h>
void handler(int sig) { printf("Got signal %d\n", sig);}
int main() { // What does this actually return? void (*result)(int) = signal(SIGINT, handler); printf("Result: %p\n", result); return 0;}The code compiled and ran. But I didn’t understand what signal was or what it returned. I looked up the declaration again:
void (*signal(int sig, void (*handler)(int)))(int);I tried reading it from left to right. void… *signal… function call… This didn’t help.
I tried reading it from right to left. (int)… then… I got lost again.
I searched online. People mentioned something called “the spiral rule”. I tried that. You spiral around the identifier clockwise. But I kept getting confused at the parentheses.
How to solve it?
The real rule
I found the actual rule on Stack Overflow. It’s not a spiral. It’s simpler:
Go right when you can. Go left when you must.Let me apply this to the signal declaration:
void (*signal(int sig, void (*handler)(int)))(int);Start at signal (the identifier):
- Look right:
(int sig, void (*handler)(int))→ function takingintand a function pointer - Can’t go further right (hit closing paren) → Go left
- Look left:
*→ pointer to… - Hit opening paren → Go right again
- Look right:
(int)→ function takingint - Can’t go further right → Go left
- Look left:
void→ returning void
So signal is: a function that takes an int and a function pointer, and returns a function pointer
The parameter void (*handler)(int) itself breaks down as:
- Start at
handler - Right:
(int)→ function takingint - Left:
*→ pointer to… - Left:
void→ returning void
So handler is: a pointer to a function taking int and returning void
Practice with simpler examples
I practiced with simpler declarations first:
int i; // i is an intint *pi; // pi is a pointer to intint **ppi; // ppi is a pointer to pointer to intint f(); // f is a function returning intint *f(); // f is a function returning pointer to intint (*f)(); // f is a pointer to function returning intThe last one confused me at first. Let’s trace it:
int (*f)();Start at f:
- Right:
()→ function - Left:
*→ pointer to… - Left:
int→ returning int
So f is a pointer to a function returning int.
But this one:
int *f();Start at f:
- Right:
()→ function - Left:
*→ but wait,*is NOT in parens - Left:
int→ returning pointer to int
So f is a function returning pointer to int.
The parentheses around *f change everything!
Why does C use this syntax?
I wondered why C has such confusing syntax. I read Dennis Ritchie’s paper “The Development of the C Language”. He explained:
The declaration syntax mirrors the usage syntax.
In expressions, you write *pi to dereference a pointer. So in declarations, you write int *pi to declare it.
In expressions, you write f() to call a function. So in declarations, you write int f() to declare it.
This made sense in 1972. C compilers ran on machines with 64KB of memory. The parser needed to be simple. Making declarations match expressions saved complexity in the compiler.
But in 2024, we have powerful computers. The trade-off makes less sense.
Using typedef to simplify
I discovered that typedefs make complex declarations readable:
// Without typedef (hard to read)void (*signal(int sig, void (*handler)(int)))(int);
// With typedef (clearer)typedef void (*sighandler_t)(int);sighandler_t signal(int sig, sighandler_t handler);Much clearer! sighandler_t is a pointer to a function taking int and returning void.
Using cdecl tool
I found a tool called cdecl that reads C declarations:
$ cdecl explain "int (*f)()"declare f as pointer to function returning int
$ cdecl explain "void (*signal(int, void (*)(int)))(int)"declare signal as function (int, pointer to function (int) returning void) returning pointer to function (int) returning voidYou can install cdecl:
# macOSbrew install cdecl
# Ubuntu/Debiansudo apt-get install cdecl
# Or use online: cdecl.orgCommon patterns
I learned to recognize these patterns:
// Array of pointersint *api[10]; // api is an array of pointers to int
// Pointer to arrayint (*pai)[10]; // pai is a pointer to an array of int
// Function returning pointerint *fp(); // fp is a function returning pointer to int
// Pointer to functionint (*pf)(); // pf is a pointer to a function returning int
// Array of function pointersint (*apf[10])(); // apf is an array of pointers to functions returning intThe key is: parentheses change the binding. * binds loosely, () binds tightly. So you need parentheses to force * to bind to the identifier first.
The absurd example
I found this example on unixwiz.net:
char *(*(**foo[][8])())[];I tried to read it using the right-left rule:
- Start at
foo - Right:
[][8]→ array of arrays (2D) - Left:
*→ pointer to… - Left:
*→ pointer to… - Left:
*→ pointer to… - Hit paren → Go right
- Right:
()→ function - Left:
*→ pointer to… - Left:
*→ pointer to… - Right:
[]→ array - Left:
char→ of char
So foo is: an array of arrays of pointers to pointers to pointers to functions returning pointers to pointers to arrays of char
This is syntactically valid C. But no sane programmer would write this.
The reason
I think the key reason C’s syntax is so complex is:
- Historical constraints: 1970s computers needed simple compilers
- Expression-based design: Declarations mirror usage
- Backward compatibility: Too much legacy code to change
The syntax made perfect sense in 1972. Today, it’s a historical artifact.
How modern languages fixed this
Modern languages learned from C’s mistakes.
Go: Clean and simple
// C: void (*signal(int, void (*handler)(int)))(int);func signal(sig int, handler func(int)) func(int)Go reads left-to-right. The func keyword makes it obvious. No confusion about what’s a pointer vs what’s a function.
Rust: Similar approach
// fn(input) -> outputtype Handler = fn(i32) -> ();fn signal(sig: i32, handler: Handler) -> HandlerRust is also left-to-right. The -> arrow separates input from output. Clear and explicit.
C++: Middle ground with templates
#include <functional>
std::function<void(int)> signal(int sig, std::function<void(int)> handler);C++ uses templates for type safety. More verbose but very clear.
Comparison
| Language | Example | Readability | Flexibility |
|---|---|---|---|
| C | void (*f)(int) | 2/10 | 10/10 |
| Go | func(int) | 9/10 | 8/10 |
| Rust | fn(i32) -> void | 9/10 | 9/10 |
| C++ | std::function<void(int)> | 7/10 | 10/10 |
Go and Rust sacrificed some flexibility for readability. This is the right trade-off for most applications.
Practical tips
Based on my experience, here’s how to handle C’s complex syntax:
- Use typedefs: Break down complex declarations into named types
- Use cdecl: Let the tool parse declarations for you
- Learn the right-left rule: Practice on simple examples first
- Add comments: Document what complex declarations mean
- Avoid absurd nesting: If you need 5 levels of pointers, rethink your design
Summary
In this post, I explained why C’s function pointer syntax is so complex and how to read it. The key point is that C’s syntax comes from historical constraints and an expression-based design philosophy. Modern languages like Go and Rust learned from this and chose readability over minimalism.
To survive C’s complex declarations:
- Use the “go right when you can, go left when you must” rule
- Leverage typedefs to simplify complex declarations
- Use tools like cdecl when you’re stuck
- Remember that this complexity is historical, not necessary
Understanding the history helps you accept the syntax. But you don’t have to like it.
Final Words + More Resources
My intention with this article was to help others share my knowledge and experience. If you want to contact me, you can contact by email: Email me
Here are also the most important links from this article along with some further resources that will help you in this scope:
- 👨💻 How do I read C declarations? - Stack Overflow
- 👨💻 Reading C type declarations - Unixwiz
- 👨💻 The Development of the C Language - Dennis Ritchie
- 👨💻 cdecl: C gibberish translator
- 👨💻 Go Language Specification: Function types
Oh, and if you found these resources useful, don’t forget to support me by starring the repo on GitHub!
Comments