From what I have understood, declarations/initializations in C++ are statements with \'base type\' followed by a comma separated list of declarators.
Consider the follow
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:
static
, extern
)virtual
, inline
)friend
, typedef
, constexpr
int
, short
)const
, volatile
)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:
i
in int i;
)*
, &
, &&
, pointer-to-member syntax)(int, char)
)[2][3]
)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:
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
."
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.
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
.
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
.
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.