C declarator understanding

Viewed 248

I am reading about declarators in C99 ISO Standard and I am struggling to understand the following passage:

5 If, in the declaration ”T D1”, D1 has the form

    identifier

then the type specified for ident is T.

6 If, in the declaration “T D1”, D1 has the form

    ( D )

then ident has the type specified by the declaration “T D”. Thus, a declarator in parentheses is identical to the unparenthesized declarator, but the binding of complicated declarators may be altered by parentheses.

4 Answers

You omitted an important previous paragraph:

4 In the following subclauses, consider a declaration

    T D1

where T contains the declaration specifiers that specify a type T (such as int) and D1 is a declarator that contains an identifier ident. The type specified for the identifier ident in the various forms of declarator is described inductively using this notation.

So, when we get to paragraphs 5 and 6, we know the declaration we are considering contains within it some identifier which we label ident. E.g., in int foo(void), ident is foo.

Paragraph 5 says that if the declaration “T D1” is just ”T ident”, it declares ident to be of type T.

Paragraph 6 says that if the declaration “T D1” is just ”T (ident)”, it also declares ident to be of type T.

These are just establishing the base cases for a recursive specification of declaration. Clause 6.7.5.1 goes on to say that if the declaration “T D1” is ”T * some-qualifiers D” and the same declaration without the * and the qualifiers, ”T D” would declare ident to be “some-derived-type T” (like “array of T” or “pointer to T”), then the declaration with the * and the qualifiers declares ident* to be “some-derived-type some-qualifiers pointer to T”.

For example, int x[3] declares x to be “array of 3 int”, so this rule in 6.7.5.1 tells us that “int * const x[3] declares x to be “array of 3 const pointer to int”—it takes the “array of 3” that must have been derived previously and appends “const pointer to” to it.

Similarly, clauses 6.7.5.2 and 6.7.5.3 tell us to append array and function types to declarators with brackets (for subscripts) and postfix parentheses.

In the quoted definitions, T is a type (e.g. int or double or struct foo or any typedef name), and D1 is a declarator.

Declarators may be arbitrarily complex, but are constructed from a small number of simple steps. They mention that it is defined inductively, which is another way of saying it has a recursive defintion.

The simplest declarator is just an identifier name, such as x or foo. So T could be int and D1 could be x.

It then goes on to say that a declarator may be parenthesized, e.g. (x) or ((x)) etc. These are degenerate cases, and are both equivalent to just x, but there are times when parentheses are needed to produce the desired grouping. For example, the declarators *x[10] and (*x)[10] mean quite different things. The former is equivalent to *(x[10]) and is an array of pointers, while the latter is a pointer to an array.

There is more to it (arrays, pointers, functions, etc.), but this covers the portion referenced in the question.

In long int *ident[4]; (array (4) of pointer to long int) long int is the specifier list, *ident[4] is the declarator. You can put the declarator in parentheses without changing semantics: long int (*ident[4]);, but in a more complex declaration such as long int (*ident[4])[5]; (array (4) of pointer to array (5) of long int), the parentheses affect binding as without them, long int *ident[4][5]; would be interpreted as array (4) of array (5) of pointer to long int.

It works this way because the declarator part can recursively encompass another declarator and so on.

From the old C manual (it got a bit more indirect in later standardized Cs, but the basic principle is the same):

declarator:
   identifier
   * declarator
   declarator ( )
   declarator [ constant-expression opt ]
   ( declarator )

To put it in another way, C declarators are a way to prefix pointer-to/function-returning/array-of to some specifier list (e.g., long int) and these can be combined to create a chain.

Normally, in the creation of that chain, the suffixes (()=function returning, []=array of) bind tighter than the */pointer-to prefix. You can use parentheses to override this and force * (or several of them) to bind now without it getting overpowered by a []/() suffix.

E.g.:

 long int *ident[4][5]; //array (4) of array (5) of pointer to long int
 //the suffixes win over the `*` prefix


 long int (*ident[4])[5]; //array (4) of pointer to array (5) of long int
 //the parens force `pointer to` right after `array (4)` 
 //without letting the `[]` suffix overpower the pointer-to/`*` declarator prefix

P.S.:

  1. C disallows arrays of functions and functions returning functions or arrays. These are both nicely expressible in the grammar but C's semantic check will want you to but a pointer-to link in there to prevent these.
  2. cdecl.org may be very helpful if you want to play with these, and perhaps even more so because it doesn't do said semantic check, therefore allowing you even things like int foo[]()(); (array of function returning function returning int), which nicely demonstrate how the grammar works, even though a C compiler would reject them.

...binding of complicated declarators

This is a very nice hint in the specs.

The rule itself is really hard to analyze because of it's recursiveness. Also the relation and parts of declaration and declarator are relevant.

The results are:

  • () and [] are the innermost direct-declarator parts, declaring (directly by symbol) functions and array names to the left
  • * declares a name as pointer on the right
  • (...) is needed for...complicated cases, to change the default association.

The grouping parens lead you on your way from inside (identifier) to outside (type specifier on the left, say "int").

In the end it is all about the pointer symbol * and what it refers to. The reformulated (brackets mean optional, not array here!) syntax is:

declarator: [* [qual]] direct-declarator
direct-declarator: (declarator)

foo() is a DD (direct declarator)

*foo is a declarator. ("indirect" by deduction)

*foo() is *(foo()). foo stays a function, () and [] bind strongest. The * is the return type.

(*foo)() makes foo a pointer. One to a function.

BTW this also explains why in a list of declarator.

int const * a, b

both are const int, but only a is a pointer

The const belongs to int and the star only to a. This makes it more clear,

const int x, *pi

But this already is borderline obfuscation. Like modern poetry. Good for certain occasions.


Even without parens there is a slight U-turn in parsing. But this is natural:

3   2 0  1
int *foo()

This standard situation (and similar ones) had to be simple. Also the famous multidim arrays like int a[10][10][10].

3    1 0  2
int (*foo)()

Here the parens force "foo" to be what is on the left side (a pointer).


Complicated Declarations have their own chapter in K&R C book.

This is sort of the simplest complicated declaration:

int (*(*foo)[])() 

It debugs to abstract type:

int (*(*)[])() 

With (*) replaced:

int (*F[])() 

The missing array size gives compiler warning - "assuming one element".

As abstract type:

int (*[])()

But:

int *G[]()

--> error: declaration of 'G' as array of functions

Yes, you can, even recursively, but with the * indirection and parens. This makes an onion of parens with the identifeir in the middle, stars on the left and [] and () on the right.


The C11 specs has this monster. The ... declare variadic args:

int (*fpfi(int (*)(long), int))(int, ...)

With all params removed:

int (*fpfi())()

Simply a function returning a pointer. One to a function returning int.

But the first param of fpfi is a function itself - a pointer to function with return type and its own params:

int (*)(long)

Non-abstractly:

int (*foo)(long)

A pointer to a function that "converts" a long to int, formally.

That is the param. Only. The return value is also a function pointer, and the pointed-to function's params and return type are outermost. Dropping the whole inner function thing (int (*)(long), int):

int (*pfi)(int, ...)

Or more generic/incomplete:

int (*pfi)()

"T (D)" Onion Rule

So this onion-game repeats itself. Inside-out and right-left-right-left between [], () and *. Syntax is not the problem, but semantics.

Related