The following is invalid code:
int i = 0, double j = 2.0;
The draft standard says why:
[N4140/7.1.6]
2
As a general rule, at most one type-specifier is allowed in the complete decl-specifier-seq of a declaration or in a type-specifier-seq or trailing-type-specifier-seq. The only exceptions to this rule are the following:—
const
can be combined with any type specifier except itself.—
volatile
can be combined with any type specifier except itself.—
signed
orunsigned
can be combined withchar
,long
,short
, orint
.—
short
orlong
can be combined withint
.—
long
can be combined withdouble
.—
long
can be combined withlong
.
Yes, it prevents something silly like int int
, but I don't see anything wrong with the invalid code posted above. Quoting [N4140/7]
, a simple-declaration consists of a decl-specifier-seqopt init-declarator-listopt;
[N4140/8]
then shows that an init-declarator-list consists of an init-declarator-list , init-declarator,
and an init-declarator is a declarator initializeropt.
Since we're concerned with only syntax of the form int i = 0
, then the declarator we care about is a ptr-declarator, which is a noptr-declarator, which is a declarator-id attribute-specifier-seqopt and finally a declarator-id consists of merely ...
optid-expression.
For completeness, [N4140/5.1.1]
says an id-expression can be an unqualified-id, or simply an identifier.
If I haven't tripped up so far, this is what the grammar reflects.
int
decl-specifier-seq
i
unqualified-id
= 0
initializer
int i = 0
init-declarator
Since the simple-declaration has the decl-specifier-seq, only one decl-specifier-seq applies to the entire init-declarator-list.
Funnily enough, that means you can't do something like this:
int i, const j;
Yet:
int i, * j;
is perfectly legal because the star is part of a ptr-operator. But you can't do this:
int i, const * j; // pointer to const int
This means in the following code that i
becomes a pointer to const int. Surprise!
int h = 25;
int const * j, * i = &h;
*i = 50; // error: assignment of read-only location '* i'
The intent is clear in [N4140/8]
with:
3
Each init-declarator in a declaration is analyzed separately as if it was in a declaration by itself.9999) A declaration with several declarators is usually equivalent to the corresponding sequence of declarations each with a single declarator. That is
T D1, D2, ... Dn;
is usually equivalent to
T D1; T D2; ... T Dn;
The question is why is it this way?
If it was legal, you could do it in for loops, which is somewhat useful.
Short answer: one statement can allow only 'one declaration of a type' but this declaration can declare 'multiple identifiers'. Also const/volatile are either type qualifiers or pointer qualifiers so they need a type or a pointer to bind to.
Long answer:
I haven't ever read the standard but here I go...
"it prevents something silly like int int, but I don't see anything wrong with the invalid code posted above."
But you went ahead and dug deeper and I believe your confusion began here onward "Since we're concerned with only syntax of the form...". Wouldn't the declaration break up as follows?
int i = 0 ::= simple-declaration
Where in...
int ::= type-specifier
i ::= identifier
= 0 ::= init-declarator
More
You mentioned...
Not Allowed: int i, const j;
Allowed: int i, * j;
Not Allowed: int i, const * j; // pointer to const int
Allowed: int const * j, * i = &h;
My response:
Not Allowed: int i, const j; - because const is a type modifier, syntactically there is no type specified to bind to.
Allowed: int i, * j; - because * grabs the type int and j has a complete type.
Not Allowed: int i, const * j; - because const is not associated to a type here. It is the same problem as in the first case. Read it as j is a pointer to <unexpected word in between> and thus screws up the grammar.
Allowed: int const * j, * i = &h; - because syntactically const has a type to bind to.
"The question is why is it this way?"
When I was learning C I was initially confused with the use of const before / after the type name and to clear the confusion I tried some test code and figured out what the language allows and the following is what I came up with. It is from my old notes. It definitely looks like something made by a new programmer. However, it clears most of the doubts.
[storage class] [sign qualifier] [size qualifier] [type qualifiers] <[* [type qualifiers]] [symbol name] [[] | ([parameters])]>
storage classes: auto, register, static, extern, typedef
sign qualifiers: signed, unsigned
size qualifiers: short, long, long long
basic types: char, int, float, double, void
type qualifiers: const, volatile
symbol name could be a variable, constant, type(def) name and function
A * prefixed to the symbol makes it a pointer. * can appear N number of times, making it pointer-to-pointer and so on.
A [] suffixed to the symbol makes it an array. [] can appear N number of times, making it a multi-dimension array.
A () suffixed to the symbol makes it a function. () can appear N number of times but since a function cannot return a function, () can appear again when a function returns a function pointer.
The above helped me think straight when declaring variables.
Modifying the type specifier syntax from my age old notes:
[storage class] [sign qualifier] [size qualifier] <type> [type qualifiers] [* [pointer qualifiers]] [symbol name] [[] | ([parameters])]
That is const and volatile are either a type qualifier or a pointer qualifier and they need a type or a pointer to bind to qualify them.
Consider the idea of "one statement can allow you only one declaration but one declaration can allow you to declare multiple identifiers of the same type." This means the type specifier syntax can be broken down as follows:
type ::= [storage class] [sign qualifier] [size qualifier] <type> [type qualifiers]
symbol ::= [* [pointer qualifiers]] [symbol name] [[] | ([parameters])]
And a simplified syntax of a declaration would be:
type symbol[, symbol...]
Clearly,
int i, const j; - does not agree with the grammar.
int const i, j; - agrees with the grammar.
I am sure a person good with the standard can use the standard and provide the answer using the correct terminology and references. However, please keep in mind that less experienced programmers might find a less technical answer easy to understand.
If the form "int i, const j" is allowed then one can write "int const i, const j" and that would means j is a double constant. That does not make any sense.
The question is why is it this way?
Unfortunately, I can't answer this with certainty. My guess is that it is a short-hand that came about to save keystrokes in C or one of its predecessors. I will say though that the *
operator, IMHO, does change the type of the variable you have declared and so I don't know why it is allowed where const
is not.
I would also like to add an example that you have not included which is legal:
int i = 0, * const j = &i;
And it is legal because the const
, in this instance, is being applied to the *
and not the int
. At least according to ideone.
It is likely that this is just legacy which needed to be brought forward.
Your extract from the standard sheds some light on the topic:
99) A declaration with several declarators is usually equivalent to the corresponding sequence of declarations each with a single declarator. That is
T D1, D2, ... Dn;
is usually equivalent to
T D1; T D2; ... T Dn;
The intent seems to be that the Dn
's when declared together separated by commas would be the same type. Because if you're changing types you might as well use a semicolon, since this saves you no keystrokes i.e.
int i, j = 0; double k, l = 7.3;
Here you've saved yourself typing int
and double
twice by using the comma in the right place.
I can see your point with the type modifiers like const
, volatile
etc. Why not allow us to combine those at will? I mean how is this different from the *
? I don't think that it is different and I hope that someone smarter than me will come and explain why. I would say either ban the *
or allow us to use other modifiers too.
So in short, I don't know, but here's a cool additional example of the craziness:
int i = 0, * const j = &i;
The question is why is it this way?
Because that's how it was in C at the Dawn of Time (1973 actually) see here
If it was legal, you could do it in for loops, which is somewhat useful.
It is legal, just not very pretty:
for(std::tuple<int, double> t = std::make_tuple(0, 10.0) ;
std::get<0>(t) < 10 ;
++std::get<0>(t), std::get<1>(t) *= 1.1)
{
cout << std::get<0>(t) << ", " << std::get<1>(t) << std::endl;
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With