60 How to read a declaration — two readings and typedef
What to know first
Looking back
While learning function pointers in chapter 59 a declaration like int (*(*s)(void))(int); appeared, and we passed on saying only “it is hard to read, so use a typedef”. But by what rule are such declarations built — why do they twist like this?
A. Because a C declaration is not writing down a type but writing down how that name is used. Dennis Ritchie’s design principle was that declaration reflects use. int *p; reads “*p is an int”, and int a[3]; reads “a[3] is an int”. This principle is elegant in the simple cases, but once * and [] and () overlap, parentheses intrude because of precedence and it quickly turns rough. Hence the separate need for rules for reading.
The need for this chapter, and its context
By the end of this chapter
char *(*table[4])(int). There are only two principles: reading from the outside in, and reading from the innermost name outward. That the two have different uses, and how to unfold this rough declaration in layers with typedef. Finally we introduce a tool that does the job for you.The questions this chapter answers
- This order is hard to memorise. Is there no simpler knack?
- I have heard of a “clockwise spiral rule” too — is it the same thing?
typedef’s syntax is odd — why is it nottypedef newname = type?- Then why practise reading such declarations at all?
- If a tool exists, must one bother learning to read by hand?
60.1 The two readings
One rule suffices to begin. The three symbols attaching beside the name in a declaration have different strengths.
[](array) and()(function) attach to the right of the name, and*(pointer) attaches to the left.- The two on the right are stronger than the one on the left. So
int *a[3]is “an array of pointers”, not “a pointer to an array”. - To reverse the order, bind with parentheses —
int (*a)[3].
On this difference of strength the two readings arise.
Reading ① — from the innermost name outward (the right-left rule). Find the identifier, start there, and go outward alternately, looking right first and then left. On meeting a parenthesis, read all of its inside and then step out. The charm of this method is that what you say as you read connects in English word order.
Reading ② — from the outside in. Start from the type name (the leftmost thing, such as int or char) and wrap your way in asking “this is a what of a what”. In a declaration with no identifier at all — the abstract declarator written in a cast or in sizeof — there is no name to start from, so this is the only road.
The two readings arrive at the same conclusion. Use whichever suits the occasion.
60.2 Reading ① in practice — connecting it in English word order
examples-en/ch60/decl.c
/* The two ways of reading a declaration, confirmed with the eyes.
Whether the type built up out of typedefs really is the original one is
checked too. */
#include <stdio.h>
/* (1) the difference of one star ──────────────────────── */
int *pa[3]; /* pa: array[3] of pointer to int */
int (*ap)[3]; /* ap: pointer to array[3] of int */
/* (2) when a function joins in ─────────────────────────── */
int *f(void); /* f: function(void) returning pointer to int */
int (*g)(void); /* g: pointer to function(void) returning int */
/* (3) the notorious shape: array[4] of pointer to function(int) returning pointer to char */
char *(*table[4])(int);
/* (4) the same type, built one typedef layer at a time */
typedef char *charptr; /* pointer to char */
typedef charptr handler(int); /* function(int) returning charptr */
typedef handler *handler_ptr; /* pointer to that function */
typedef handler_ptr table4[4]; /* array[4] of that pointer */
/* whether the types built by the two roads really match, checked at compile time */
static_assert(sizeof(table4) == sizeof(table), "they must be the same type");
static char *shout(int n) { (void)n; return "shout"; }
static char *quiet(int n) { (void)n; return "quiet"; }
int main(void)
{
printf("int *pa[3] : whole %zu, element %zu -> %zu pointers\n",
sizeof pa, sizeof pa[0], sizeof pa / sizeof pa[0]);
printf("int (*ap)[3] : whole %zu, what it points to %zu\n",
sizeof ap, sizeof *ap);
/* (3) actually filled in and used */
table[0] = shout;
table[1] = quiet;
printf("table[0](1) = %s, table[1](2) = %s\n", table[0](1), table[1](2));
/* a variable made with (4)'s typedefs fits the same slot as it is */
table4 other = { quiet, shout };
printf("other[0](3) = %s (the same type, built with typedefs)\n", other[0](3));
/* the form without an identifier (an abstract declarator): used in casts and sizeof */
printf("sizeof(char *(*)(int)) = %zu (a nameless function pointer type)\n",
sizeof(char *(*)(int)));
return 0;
}
Output
int *pa[3] : whole 24, element 8 -> 3 pointers
int (*ap)[3] : whole 8, what it points to 12
table[0](1) = shout, table[1](2) = quiet
other[0](3) = quiet (the same type, built with typedefs)
sizeof(char *(*)(int)) = 8 (a nameless function pointer type)
We read the example’s declarations one at a time. What you say is most natural strung together in English — because C’s declaration syntax was designed to follow English word order.
int *pa[3];
- Start from the identifier
pa— “pa is” - Look right:
[3]— “array 3 of” - The right is finished, so look left:
*— “pointer to” - What remains:
int— “int”
Read through: “pa is array 3 of pointer to int” — an array of three pointers. The example confirmed it with 24 bytes in total and 8 bytes per element.
int (*ap)[3];
- Start from
ap— “ap is” - To the right is the end of the parenthesis, so we cannot go. Look left:
*— “pointer to” - Step out of the parenthesis. Right again:
[3]— “array 3 of” - What remains:
int
“ap is pointer to array 3 of int” — one pointer (8 bytes), and what it points at is a 12-byte array. That is the example’s second line.
char *(*table[4])(int); — this chapter’s protagonist.
- Start from
table— “table is” - Right:
[4]— “array 4 of” - Left:
*— “pointer to” - Outside the parenthesis, right:
(int)— “function (int) returning” - Left:
*— “pointer to” - What remains:
char
“table is array 4 of pointer to function (int) returning pointer to char” — a table of functions taking one integer and giving back a string. The example actually put two functions in and called them.
60.2.1 This reading has a name — boustrophedon
Look again at what we just did: the eye traced a zigzag, right → left → right. That movement has an old name. It is boustrophedon, from the Greek βοῦς (ox) and στρέφειν (to turn) — the shape an ox makes drawing a plough to the end of a field, then turning back the other way. Ancient Greek inscriptions were sometimes cut this way, one line left-to-right and the next right-to-left, and that is called boustrophedonic writing.
The person who first called C declarations this was Peter van der Linden. He wrote — “Declarations in C are read boustrophedonically, i.e. alternating right-to-left with left-to-right. And who’d have thought there would be a special word to describe that!”1
There is one good reason to know the name. It carries the fact that reading ① is not an arbitrary knack but a procedure with a shape. The same book compresses that procedure into a precedence rule. It is what we spelled out in sentences above, except that the third item has not come up yet.
| Clause | Content |
|---|---|
| A | Declarations are read starting from the name, in order of precedence |
| B.1 | Highest: parentheses grouping parts of the declaration |
| B.2 | Then: the postfix operators — () (function) and [] (array) |
| B.3 | Then: the prefix operator — * (“pointer to”) |
| C | If const/volatile is next to a type specifier (int, long, …) it applies to that type. Otherwise it applies to the asterisk immediately to its left |
Table 61.1
★ Clause C causes the most accidents in practice, because where the const lands flips the meaning completely.
examples/ch60/precedence.c
/* 우경식(牛耕式) 읽기의 우선순위 규칙을 눈으로 확인한다.
특히 const/volatile 이 「왼쪽의 별표에 붙는가, 타입에 붙는가」를 실물로 가른다. */
#include <stdio.h>
static int one = 1, two = 2;
int main(void)
{
/* ① const 가 타입 지정자 옆에 있다 → 타입에 붙는다(가리키는 것이 읽기 전용) */
const int *pci = &one; /* 「int const 를 가리키는 포인터」 */
int const *pci2 = &one; /* 위와 완전히 같다 — 순서만 다르다 */
/* ② const 가 타입 지정자 옆이 아니다 → 바로 왼쪽의 별표에 붙는다 */
int *const cpi = &one; /* 「int 를 가리키는, 읽기 전용 포인터」 */
/* ③ 둘 다 */
const int *const cpci = &one;
/* ①은 가리키는 곳을 바꿀 수 있다. *pci = 9; 는 오류다. */
pci = &two;
pci2 = &two;
printf("(1) *pci=%d *pci2=%d <- the pointer itself can move\n", *pci, *pci2);
/* ②는 반대다. cpi = &two; 는 오류이고, 가리키는 값은 바꿀 수 있다. */
*cpi = 42;
printf("(2) *cpi=%d one=%d <- the value can be changed\n", *cpi, one);
printf("(3) *cpci=%d <- read-only on both sides\n", *cpci);
/* ④ 우경식으로 읽는 실물: 오른쪽 먼저, 막히면 왼쪽 */
char *const *next = NULL; /* next 는 「char 를 가리키는 읽기 전용 포인터」를
가리키는 포인터 */
printf("(4) sizeof next = %zu (one pointer)\n", sizeof next);
/* ⑤ 태그를 붙여 두었기에 자기 자신을 가리킬 수 있다 */
struct node_tag { int datum; struct node_tag *next; };
struct node_tag b = { 2, NULL };
struct node_tag a = { 1, &b };
printf("(5) a.datum=%d -> a.next->datum=%d\n", a.datum, a.next->datum);
return 0;
}
Output
(1) *pci=2 *pci2=2 <- the pointer itself can move
(2) *cpi=42 one=42 <- the value can be changed
(3) *cpci=42 <- read-only on both sides
(4) sizeof next = 8 (one pointer)
(5) a.datum=1 -> a.next->datum=2
The example’s first three lines are that fork.
| Declaration | Read by clause C | What is read-only |
|---|---|---|
const int *p | const is next to int → applies to the type | the pointed-to value |
int const *p | the same — only the order differs | the pointed-to value |
int *const p | const is not next to a type → applies to the * on its left | the pointer itself |
const int *const p | both | value and pointer alike |
Table 61.2
The example split these apart for real. const int *pci can be moved with pci = &two;, and int *const cpi can change the value with *cpi = 42;. The other way round is a compile error in each case: assignment of read-only location '*pci' and assignment of read-only variable 'cpi'.
Q. This order is hard to memorise. Is there no simpler knack?
A. There is. const qualifies the thing immediately to its left. If there is nothing to its left, it qualifies the thing to its right. That one sentence says what clause C above says.
This is why some people write const on the right always — int const *p, the so-called “east const” convention. Then a single rule with no exception remains: it qualifies what is to its left. But the standard headers and most existing code write const int *, so for reading you must know both.
60.2.2 Reading by erasing
The same book recommends one more knack: erase what you have read. Skimming with the eye alone loses track of how far you have got; erase each piece as you handle it and what remains is exactly the next job. That is why reading with pen on paper almost never goes wrong.
Read char *const *(*next)(); that way. The bold piece is the one in hand.
| Declaration remaining | What was seen | Sentence so far |
|---|---|---|
char *const *(*next)(); | the identifier | next is |
char *const *(*)(); | the * inside the parens | … pointer to |
char *const *(); | the outer () | … function() returning |
char *const *; | * | … pointer to |
char *const; | clause C — attaches to the * on its left | … read-only |
char *; | * | … pointer to |
char | the type left over | … char |
Table 61.3
Joined up: “next is a pointer to a function returning a pointer to a read-only pointer to char.” This declaration is not invented — it comes from the source of telnet.
Q. I have heard of a “clockwise spiral rule” too — is it the same thing?
A. It is the same thing drawn to be easy to memorise, and it mostly works. But there has long been the objection that the spiral rule, being a summary of the right-left rule, is drawn confusingly for some forms. Written exactly, the rule is this — start from the identifier, read right as far as you can go, and when you can go no further read left. Parentheses are the boundary. Use the spiral picture as a visual nickname for that sentence.
60.3 Reading ② — when there is no name
Casts and sizeof take a type written without a name. The last line of the example, sizeof(char *(*)(int)), is that. Such a form is called an abstract declarator, and the way to read it is simple — find the place where the name ought to be, lay a name there, and read by reading ①.
char *(*)(int) /* there is no name */
char *(*x)(int) /* put x in the empty place and */
/* "x is pointer to function (int) returning pointer to char" */The empty place is usually after the star of (*), or before [] or (). Thanks to this knack, reading ①′s muscle can be used as it is.
Then when is reading ② used. When the shells are several layers deep — as in a declaration taking a function pointer as an argument — and you want to know first which is the outermost.
void qsort(void *base, size_t n, size_t size,
int (*cmp)(const void *, const void *));Reading from the outside in, the skeleton is grasped first: “this is a function qsort, returning void, with four arguments of which the last is a function pointer.” The details are then confirmed with reading ①. When reading somebody else’s header in the field this order is the comfortable one.
60.4 typedef — dividing into layers and naming them
The most practical answer to a rough declaration is dividing it into layers by naming them. The example’s ④ is that demonstration.
typedef char *charptr; /* pointer to char */
typedef charptr handler(int); /* function(int) returning charptr */
typedef handler *handler_ptr; /* pointer to that function */
typedef handler_ptr table4[4]; /* array[4] of that pointer */Each of the four lines lays on just one layer. The final table4 is exactly the same type as the example ③′s char *(*table[4])(int), and the example’s static_assert confirms it at compile time. In the field it is more common to name only one or two layers than to slice this finely.
typedef char *(*handler_fn)(int); /* when one layer is enough */
handler_fn table[4];60.4.1 Where to use it and where not to
We know how to divide into layers; the question is when. On that there is a rule of thumb that has been quoted for over thirty years — the three items van der Linden put in a box called “Tips for Working with Typedefs”.2
In practice. Van der Linden's three rules of thumb (1994)
① Do not bother with typedefs for structs. All they do is save you writing the word struct, which is a clue that you probably shouldn’t be hiding anyway.
② Use typedefs for three things. Types that combine arrays, structs, pointers, or functions. Portable types — when you need a type that is at least (say) 20 bits, make it a typedef and change that one line when you port. And names for complicated casts.
③ Always use a tag in a structure definition, even if it is not needed. It will be later.
Take them one at a time against today.
① still holds. It is exactly the antipattern box above — hiding a struct behind a typedef blurs where the const attaches, and that is the price of the hiding. The Linux kernel’s coding conventions reach the same conclusion. The one exception is the opaque type, where hiding is the design.
② is what we are already doing, with one of today’s facts to add. “Make it a typedef when you need at least 20 bits” was, in 1994, something you had to build yourself. Today the standard does it for you — <stdint.h>‘s int32_t, int_least32_t and int_fast32_t are typedefs made for precisely that purpose (chapter 27). ★ So today’s version reads: look first for one the standard already has. Build one only when there is none.
③ turns out to be right for two reasons. One is van der Linden’s “it will be later.” The other is that there are cases where it is needed right now — a struct that points at itself.
typedef struct { int datum; node *next; } node; /* does not work */The name node does not exist until the ;, so it cannot be used inside. Compile it and you get error: unknown type name 'node'. With a tag the problem goes away, because the tag exists the moment struct is followed by it.
typedef struct node_tag { int datum; struct node_tag *next; } node; /* works */The example’s ⑤ chains two nodes in this form. This is also why we have suffixed struct tags with _tag throughout the earlier chapters — so as not to use one name for two different things (chapter 55′s name spaces).
Q. typedef’s syntax is odd — why is it not typedef newname = type?
A. Because typedef is a word that comes in the position of a storage-class specifier. Grammatically typedef int count; is placed where static int count; or extern int count; would be. So read it like this — “in the place where this declaration would create a variable, a type name is created instead.”
This understanding pays in practice. Seeing typedef int arr[3]; unfolds as “had there been no typedef, arr would have been an array variable of three ints → therefore arr is the name of that type.” The declaration reading is reused as it is.
Counter-example. Hiding a pointer behind a typedef
typedef struct node *node; /* that it is a pointer vanishes from the name */
void f(const node n); /* what is the const attaching to? */const node n is not “what is pointed at is const” but “the pointer itself is const” (struct node *const). It cannot be known from the name alone, so it is commonly got wrong.
Practice splits in two. Do not hide pointers (recommended) — leave the type name as struct node and write struct node *. Or if you do hide it, mark it in the name — as in node_ptr. The Linux kernel’s coding conventions pinned down “do not use pointer typedefs” for the same reason.
There is an exception, though. When making an opaque type you hide it on purpose — in a handle whose innards the user must not know (the position of FILE *, say), hiding is the design. Just distinguish hiding from losing by accident.
A common misconception. “typedef makes a new type”
It does not. typedef makes only an alias, treated as entirely the same as the original type.
typedef int meters;
typedef int seconds;
meters d = 10;
seconds t = 5;
d = t; /* no warning — both are just int */If you hoped the compiler would catch the mistake of “assigning seconds to metres”, you will be disappointed. To distinguish types for real you must wrap them in a struct — struct meters { int v; }; and then the assignment becomes an error (thanks to chapter 46′s property that a struct is a value). A device like C++‘s strong type aliases does not exist in C.
60.5 The real thing — declarations that actually shipped
What follows is not exercise material. These are declarations from libraries that really shipped, read by the procedure. The point of the section is to confirm there is nothing to be afraid of.
examples-en/ch60/monster.c
/* Gnarly declarations that really shipped — and how a procedure unravels them. */
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
/* -- (1) the standard's signal — exactly as it stands in C §7.14.1.1:
void (*signal(int sig, void (*func)(int)))(int);
Split into layers with typedef it becomes this. */
typedef void handler_t(int); /* name the function type itself */
static void on_int(int sig) { (void)sig; }
/* -- (2) the shape of X11's XSetErrorHandler:
int (*XSetErrorHandler(int (*handler)(Display *, XErrorEvent *)))();
Copied as is, with stand-in types for Display and XErrorEvent. */
typedef struct { int dummy; } Display;
typedef struct { int code; } XErrorEvent;
static int my_error_handler(Display *d, XErrorEvent *e)
{ (void)d; printf(" error handler: code=%d\n", e->code); return 0; }
/* Raw: "a function taking a handler and returning the previous handler" */
static int (*set_error_handler(int (*handler)(Display *, XErrorEvent *)))
(Display *, XErrorEvent *)
{
static int (*current)(Display *, XErrorEvent *);
int (*prev)(Display *, XErrorEvent *) = current;
current = handler;
return prev;
}
/* The same with typedef — one line becomes three, and all three read */
typedef int error_handler_t(Display *, XErrorEvent *);
static error_handler_t *set_error_handler2(error_handler_t *handler)
{ return set_error_handler(handler); }
/* -- (3) an array of pointers to functions — a dispatch table ---------- */
static int cmd_add(int a, int b) { return a + b; }
static int cmd_mul(int a, int b) { return a * b; }
/* int (*table[2])(int, int) — an array of "pointer to function taking two
ints and returning int". Read by the procedure, that is what comes out. */
static int (*table[2])(int, int) = { cmd_add, cmd_mul };
/* -- (4) a function returning a pointer to an array -------------------- */
static int (*rows(void))[4] /* "function returning pointer to int[4]" */
{
static int grid[3][4] = { {1,2,3,4}, {5,6,7,8}, {9,10,11,12} };
return grid; /* decays to int (*)[4] */
}
int main(void)
{
/* (1) signal returns the *previous* handler — hence the function-pointer return */
handler_t *old = signal(SIGINT, on_int);
printf("signal returns the previous handler: %s\n",
old == SIG_ERR ? "SIG_ERR" : "a previous value");
(void)signal(SIGINT, old == SIG_ERR ? SIG_DFL : old);
/* (2) the same pattern — install, and get the previous one back */
puts("\ninstalling an X11-style error handler:");
int (*prev)(Display *, XErrorEvent *) = set_error_handler(my_error_handler);
printf(" previous handler: %s\n", prev ? "present" : "none (first install)");
Display d = { 0 };
XErrorEvent e = { .code = 42 };
error_handler_t *prev2 = set_error_handler2(my_error_handler); /* typedef form */
printf(" the typedef version does the same: previous handler %s\n",
prev2 == my_error_handler ? "identical" : "different");
my_error_handler(&d, &e);
/* (3) the dispatch table */
printf("\ndispatch table: add(3,4)=%d, mul(3,4)=%d\n", table[0](3, 4), table[1](3, 4));
/* (4) a pointer to an array */
int (*g)[4] = rows();
printf("pointer to array: g[2][1] = %d (one step is %zu bytes)\n",
g[2][1], sizeof *g);
return 0;
}
Output
signal returns the previous handler: a previous value
installing an X11-style error handler:
previous handler: none (first install)
the typedef version does the same: previous handler identical
error handler: code=42
dispatch table: add(3,4)=7, mul(3,4)=12
pointer to array: g[2][1] = 10 (one step is 16 bytes)
60.5.1 ① The monster the standard itself produced — signal
This declaration stands in the standard’s <signal.h> exactly as written (§7.14.1.1).
void (*signal(int sig, void (*func)(int)))(int);At first sight it is a forest of parentheses, but following the procedure of the previous section it takes five steps. Start at the name, look right first, and go left when the right is exhausted.
| Step | What is in view | The sentence so far |
|---|---|---|
| 1 | signal | signal is |
| 2 | to the right: (int sig, void (*func)(int)) | … a function taking an int and “a pointer to a function taking int and returning void” |
| 3 | to the left: * | … returning a pointer |
| 4 | outside the parentheses, right: (int) | … that pointer points at a function taking int |
| 5 | leftmost: void | … returning void |
Table 61.4
In one sentence: “signal takes a signal number and a handler, and returnsthe previous handler.” That also explains why the return type is so rough — installing must hand back the previous one so it can be restored later. The first line of the demonstration actually takes that previous handler and restores it.
One typedef makes the declaration ordinary.
typedef void handler_t(int); /* name the function type */
handler_t *signal(int sig, handler_t *func);The standard does not write it that way for historical reasons — this function existed long before layering with typedef became the habit.
60.5.2 ② Rougher in the wild — X11′s error handler
From the X Window System’s manual:
int (*XSetErrorHandler(int (*handler)(Display *, XErrorEvent *)))();The pattern is identical to signal: take a handler, return the previous one. The only difference is that the handler takes two arguments, so a layer of parentheses looks thicker. By the procedure, the number of steps is the same.
The second block of the demonstration transplants the pattern and runs it — a raw set_error_handler and a set_error_handler2 layered with typedef do the same work. The latter shows that this rough declaration is really the one line “a function taking a handler and returning a handler.”
typedef int error_handler_t(Display *, XErrorEvent *);
error_handler_t *XSetErrorHandler(error_handler_t *handler);In practice. The same pattern is everywhere
Once recognised, “take a handler and return the previous handler” shows up all over: the standard’s signal, X11′s XSetErrorHandler and XSetIOErrorHandler, and most callback-registration functions in GUI and game frameworks. The reason is the same in each — it must be possible to restore. Code that plugs a library in has no way to put things back afterwards unless installation hands back what was there.
For the same reason qsort and bsearch take a comparison function (int (*compar)(const void *, const void *)), and POSIX’s pthread_create takes a start routine (void *(*)(void *)). Most rough-looking declarations come from one idea: passing behaviour as a value.
60.5.3 ③ And the genuinely pointless ones
Declaration quizzes on the internet have their regulars.
char *(*(**foo[][8])())[]; /* an example from cdecl's own documentation */
int (*(*bar[10])(void))(int);The procedure works on these too. The first is “an array of arrays of 8 of pointer to pointer to function returning pointer to array of pointer to char”. More important than having read it is the judgement that follows: do not put such a declaration in your code.
The difference between these and the two above is this chapter’s point. The first two are declarations worth untangling — they are really used and the pattern carries meaning. The last has no meaning; it only shows what the grammar permits.
Q. Then why practise reading such declarations at all?
A. Three practical reasons.
First, you do not get to choose other people’s code. Standard headers, old libraries and kernel structures carry these declarations as they are. Unable to read one, you cannot use the function.
Second, you have to read error messages. When a function-pointer type does not match, the compiler prints types like int (*)(Display *, XErrorEvent *) verbatim. Knowing the procedure turns that message into a sentence.
Third, deciding what to wrap in a typedef requires reading it first. Give a name to something you have not understood and the name will lie.
But the purpose is reading. For writing, always divide into layers with typedef — the tool in the next section helps with that judgement; it does not replace it.
A common misconception. “Only geniuses read complicated declarations”
Not so, because there is a procedure. As the table above shows, signal is five steps and X11′s is five steps. The number of steps is set by how many layers of parentheses there are, not by anyone’s talent.
The real reasons it feels hard are two: scanning with the eye instead of following the procedure, and trying to grasp the whole meaning at once. A machine does not read that way — it peels one layer at a time and appends what it peeled to a sentence. Do the same and mistakes almost stop happening, especially with the steps written down on paper.
60.6 Leaving it to a tool — cdecl
A program that does this reading for you has existed for a long time. It is cdecl, which appeared in the 1980s and is still maintained (its current maintainer is Paul J. Lucas, GPLv3). Give it a declaration and it unfolds it into English; speak English and it builds the declaration.
cdecl> explain char *(*table[4])(int)
cdecl> declare table as array 4 of pointer to function (int) returning pointer to charThe first line does what we did by hand above, and the second is the opposite direction — it builds a C declaration from what was said in English. It can be installed as a package on Linux distributions (cdecl), and there is cdecl.org for using it in a browser without installing.
Q. If a tool exists, must one bother learning to read by hand?
A. It is worth learning for two reasons. First, reading happens constantly while opening a tool is occasional. You do not open a browser every time you meet an int (*p)[3] while skimming somebody’s code. Second, there is the writing side. A tool reads for you, but “what declaration should be written in this place” is settled in the end by whoever knows the rules — and the usual right answer is this chapter’s conclusion: do not write it roughly in one line; divide it into layers with typedef.
Recap
| to remember | the point |
|---|---|
| design principle | a declaration reflects use — hence the twisting |
| difference of strength | [], () (right) are stronger than * (left). parentheses reverse it |
| reading ① | start from the identifier → right first, then left → in English word order |
| its name | boustrophedon — the way an ox turns while ploughing |
| precedence rule | parens > postfix ()·[] > prefix * |
where const lands | ★ next to a type specifier → the type; otherwise the asterisk on its left |
| reading knack | erase each piece handled — what remains is the next job |
| reading ② | from the outside in. essential for an abstract declarator with no name |
| abstract declarator | lay a name in the empty place and read by reading ① |
typedef | a word in the storage-class position — a type name is created instead of a variable |
where to use typedef | combined types · portable types (look in the standard first) · names for complicated casts |
| struct tags | ★ always add one, even when it looks unnecessary — self-reference needs it now |
typedef’s limit | an alias only, not a new type. to distinguish, use a struct |
| hiding pointers | not recommended (the meaning of const blurs). the exception is an opaque type |
| tool | cdecl (explain/declare), cdecl.org |
Table 61.5
We have gained the muscle for reading declarations. From the next chapter we enter the terrain of the standard library — the part where, on top of the language learned so far, we see what contracts and traps the functions the world has piled up over half a century carry.