cdecl: Because void (*signal(int, void (*)(int)))(int) Shouldn't Require a Séance

2026-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.

Key Takeaway: Stop reading gnarly C declarations by hand — 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.

All newsletters