void (*signal(int, void (*)(int)))(int) Shouldn't Require a Séance2026-09-07
Somewhere on your career timeline, you stared at a C declaration like char *(*(**foo[][8])())[] and felt your prefrontal cortex quietly file for early retirement. The mechanical trick — read the identifier, spiral outward, apply precedence — works, but it's a party trick that scales poorly at 2 a.m. There is a tool. It has been sitting in your package manager since Ken Arnold wrote it for the BSD games directory in the mid-1980s, and it is called cdecl.
What it does: translates C (and C++) declarations to English, and English descriptions back into valid C. That's it. No LSP, no compiler backend, just a tiny parser and a lot of good judgment about how humans read pointers.
$ cdecl explain 'int (*(*fp)(int))[10]'
declare fp as pointer to function (int) returning pointer to array 10 of 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
That second one is the actual prototype of signal(2), and I have watched a room of senior engineers argue about it for twenty minutes. cdecl answers in a millisecond and doesn't take sides.
The reverse direction is where it earns its keep during writing code, not just reading it. Function-pointer syntax is a hazing ritual; describe what you want and let cdecl emit it:
$ cdecl declare handler as pointer to function \
'(int, pointer to function (int) returning void)' returning void
void (*handler)(int , void (*)(int ))
$ cdecl declare table as array 16 of pointer to function \
'(pointer to const char)' returning int
int (*table[16])(const char *)
$ cdecl cast p into pointer to array 8 of pointer to function returning int
(int (*(*)[8])())p
The cast subcommand alone has saved me from more segfaults than any sanitizer. If you can't describe the cast in English, you shouldn't be writing it.
Interactive mode is a REPL, so it composes cleanly with rlwrap if your build is old enough to lack readline:
$ cdecl
cdecl> set options
noalign, nobitfields, nocdecl11, cdecl-only, nodigraphs,
east-const, english-types, explain-by-default, ...
cdecl> set c++17
cdecl> explain 'auto (*p)(int) -> int (*)(double)'
declare p as pointer to function (int) returning
pointer to function (double) returning int
Modern forks (Paul J. Lucas's cdecl, the one Debian and Homebrew ship) understand C23, C++23, trailing returns, _Atomic, noexcept, references, member pointers, and the [[attribute]] spellings. Old BSD cdecl choked on anything past K&R; the modern build is a proper parser generator and it shows.
Pipe-friendly, so it lives happily in a Makefile hack or a git pre-commit hook:
# Extract every top-level declaration from a header and gloss it
$ ctags -x --c-kinds=pvf include/api.h \
| awk '{print $NF}' \
| while read decl; do echo "explain $decl"; done \
| cdecl
Why not just… read the code carefully? Because "carefully" is a finite resource and you're spending it on the wrong problem. cdecl is the compiler's own parsing rules, exposed as a shell command, so the ambiguity that trips you up (const binding left vs. right, function-vs-pointer decay in typedef position, the difference between T *const and const T *) is decided by a grammar, not by squinting.
Install cost: apt install cdecl or brew install cdecl. Learning cost: about ninety seconds. Payoff: every time you touch a signal handler, a callback registration table, or someone's clever function-returning-array typedef, you have a second opinion that never gets tired.
cdecl has been translating them to plain English (and back) since the 1980s, and it doesn't get the precedence wrong at 2 a.m.
