From what I have understood, declarations/initializations in C++ are statements with 'base type' followed by a comma separated list of declarators.

Consider the following declarations:

int i = 0, *const p = &i; // Legal, the so-called base type is 'int'.
                          // i is an int while p is a const pointer to an int.

int j = 0, const c = 2;   // Error: C++ requires a type specifier for all declarations.
                          // Intention was to declare j as an int and c an as const int.

int *const p1 = nullptr, i1 = 0; // p1 is a const pointer to an int while i1 is just an int.

int const j1 = 0, c1 = 2;   // Both j1 and c1 are const int.

Is const int a base type or a compound type?

From the error in the second declaration above, it seems to be a base type. If it is so, then what about the first declaration?

In other words, if the first statement is legal, why isn't the second one? Also, why does the behaviour differ among the third and fourth statements?


Solution 1:

Good question, with a complicated answer. To really grasp this, you need to understand the internal structure of C++ declarations quite thoroughly.

(Note that in this answer, I will totally omit the existence of attributes to prevent overcomplication).

A declaration has two components: a sequence of specifiers, followed by a comma-separated list of init-declarators.

Specifiers are things like:

  • storage class specifiers (e.g. static, extern)
  • function specifiers (e.g. virtual, inline)
  • friend, typedef, constexpr
  • type specifiers, which include:
    • simple type specifiers (e.g. int, short)
    • cv-qualifiers (const, volatile)
    • other things (e.g. decltype)

The second part of a declaration are the comma-separated init-declarators. Each init-declarator consists of a sequence of declarators, optionally followed by an initialiser.

What declarators are:

  • identifier (e.g. the i in int i;)
  • pointer-like operators (*, &, &&, pointer-to-member syntax)
  • function parameter syntax (e.g. (int, char))
  • array syntax (e.g. [2][3])
  • cv-qualifiers, if these follow a pointer declarator.

Notice that the declaration's structure is strict: first specifiers, then init-declarators (each being declarators optionally followed by an initialiser).

The rule is: specifiers apply to the entire declaration, while declarators apply only to the one init-declarator (to the one element of the comma-separated list).

Also notice above that a cv-qualifier can be used as both a specifier and a declarator. As a declarator, the grammar restricts them to only be used in the presence of pointers.

So, to handle the four declarations you have posted:

1

int i = 0, *const p = &i;

The specifier part contains just one specifier: int. That is the part that all declarators will apply to.

There are two init-declarators: i = 0 and * const p = &i.

The first one has one declarator, i, and an initialiser = 0. Since there is no type-modifying declarator, the type of i is given by the specifiers, int in this case.

The second init-declarator has three declarators: *, const, and p. And an initialiser, = &i.

The declarators * and const modify the base type to mean "constant pointer to the base type." The base type, given by specifiers, is int, to the type of p will be "constant pointer to int."

2

int j = 0, const c = 2;

Again, one specifier: int, and two init-declarators: j = 0 and const c = 2.

For the second init-declarator, the declarators are const and c. As I mentioned, the grammar only allows cv-qualifiers as declarators if there is a pointer involved. That is not the case here, hence the error.

3

int *const p1 = nullptr, i1 = 0;

One specifier: int, two init-declarators: * const p1 = nullptr and i1 = 0.

For the first init-declarator, the declarators are: *, const, and p1. We already dealt with such an init-declarator (the second one in case 1). It adds the "constant pointer to base type" to the specifier-defined base type (which is still int).

For the second init-declarator i1 = 0, it's obvious. No type modifications, use the specifier(s) as-is. So i1 becomes an int.

4

int const j1 = 0, c1 = 2;

Here, we have a fundamentally different situation from the preceding three. We have two specifiers: int and const. And then two init-declarators, j1 = 0 and c1 = 2.

None of these init-declarators have any type-modifying declarators in them, so they both use the type from the specifiers, which is const int.

Solution 2:

This is specified in [dcl.dcl] and [dcl.decl] as part of the simple-declaration* and boils down to differences between the branches in ptr-declarator:

declaration-seq:
    declaration

declaration:
    block-declaration

block-declaration:
    simple-declaration

simple-declaration:
    decl-specifier-seqopt init-declarator-listopt ;
----

decl-specifier-seq:
    decl-specifier decl-specifier-seq    

decl-specifier:    
    type-specifier                               ← mentioned in your error

type-specifier:
    trailing-type-specifier

trailing-type-specifier:
    simple-type-specifier
    cv-qualifier
----

init-declarator-list:
   init-declarator
   init-declarator-list , init-declarator

init-declarator:
   declarator initializeropt

declarator:
    ptr-declarator

ptr-declarator:                                 ← here is the "switch"
    noptr-declarator
    ptr-operator ptr-declarator

ptr-operator:                                   ← allows const
    *  cv-qualifier-seq opt

cv-qualifier:
    const
    volatile

noptr-declarator:                               ← does not allow const
    declarator-id

declarator-id:
    id-expression

The important fork in the rules is in ptr-declarator:

ptr-declarator:
    noptr-declarator
    ptr-operator ptr-declarator

Essentially, noptr-declarator in your context is an id-expression only. It may not contain any cv-qualifier, but qualified or unqualified ids. However, a ptr-operator may contain a cv-qualifier.

This indicates that your first statement is perfectly valid, since your second init-declarator

 *const p = &i;

is a ptr-declarator of form ptr-operator ptr-declarator with ptr-operator being * const in this case and ptr-declarator being a unqualified identifier.

Your second statement isn't legal because it is not a valid ptr-operator:

 const c = 2

A ptr-operator must start with *, &, && or a nested name specifier followed by *. Since const c does not start with either of those tokens, we consider const c as noptr-declarator, which does not allow const here.

Also, why the behaviour differs among 3rd and 4th statements?

Because int is the type-specifier, and the * is part of the init-declarator,

*const p1

declares a constant pointer.

However, in int const, we have a decl-specifier-seq of two decl-specifier, int (a simple-type-specifier) and const (a cv-qualifier), see trailing-type-specifier. Therefore both form one declaration specifier.


* Note: I've omitted all alternatives which cannot be applied here and simplified some rules. Refer to section 7 "Declarations" and section 8 "Declarators" of C++11 (n3337) for more information.