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.