Skip to content

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:

signal.h
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:

signal_test.c
#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):

  1. Look right: (int sig, void (*handler)(int)) → function taking int and a function pointer
  2. Can’t go further right (hit closing paren) → Go left
  3. Look left: * → pointer to…
  4. Hit opening paren → Go right again
  5. Look right: (int) → function taking int
  6. Can’t go further right → Go left
  7. 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 taking int
  • 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:

declarations.c
int i; // i is an int
int *pi; // pi is a pointer to int
int **ppi; // ppi is a pointer to pointer to int
int f(); // f is a function returning int
int *f(); // f is a function returning pointer to int
int (*f)(); // f is a pointer to function returning int

The 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:

signal_typedef.c
// 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:

Terminal window
$ 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 void

You can install cdecl:

Terminal window
# macOS
brew install cdecl
# Ubuntu/Debian
sudo apt-get install cdecl
# Or use online: cdecl.org

Common patterns

I learned to recognize these patterns:

patterns.c
// Array of pointers
int *api[10]; // api is an array of pointers to int
// Pointer to array
int (*pai)[10]; // pai is a pointer to an array of int
// Function returning pointer
int *fp(); // fp is a function returning pointer to int
// Pointer to function
int (*pf)(); // pf is a pointer to a function returning int
// Array of function pointers
int (*apf[10])(); // apf is an array of pointers to functions returning int

The 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:

absurd.c
char *(*(**foo[][8])())[];

I tried to read it using the right-left rule:

  1. Start at foo
  2. Right: [][8] → array of arrays (2D)
  3. Left: * → pointer to…
  4. Left: * → pointer to…
  5. Left: * → pointer to…
  6. Hit paren → Go right
  7. Right: () → function
  8. Left: * → pointer to…
  9. Left: * → pointer to…
  10. Right: [] → array
  11. 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:

  1. Historical constraints: 1970s computers needed simple compilers
  2. Expression-based design: Declarations mirror usage
  3. 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

signal.go
// 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

signal.rs
// fn(input) -> output
type Handler = fn(i32) -> ();
fn signal(sig: i32, handler: Handler) -> Handler

Rust is also left-to-right. The -> arrow separates input from output. Clear and explicit.

C++: Middle ground with templates

signal.cpp
#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

LanguageExampleReadabilityFlexibility
Cvoid (*f)(int)2/1010/10
Gofunc(int)9/108/10
Rustfn(i32) -> void9/109/10
C++std::function<void(int)>7/1010/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:

  1. Use typedefs: Break down complex declarations into named types
  2. Use cdecl: Let the tool parse declarations for you
  3. Learn the right-left rule: Practice on simple examples first
  4. Add comments: Document what complex declarations mean
  5. 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:

Oh, and if you found these resources useful, don’t forget to support me by starring the repo on GitHub!

Comments