Where in a declaration may a storage class specifier be placed?
Asked Answered
D

3

21

For example, let's consider the static storage class specifier. Here are a few examples of both valid and ill-formed uses of this storage class specifier:

static int a;        // valid
int static b;        // valid

static int* c;       // valid
int static* d;       // valid
int* static e;       // ill-formed

static int const* f; // valid
int static const* g; // valid
int const static* h; // valid
int const* static i; // ill-formed

typedef int* pointer;
static pointer j;    // valid
pointer static k;    // valid

(The declarations marked "valid" were accepted by Visual C++ 2012, g++ 4.7.2, and Clang++ 3.1. The declarations marked "ill-formed" were rejected by all of those compilers.)

This seems odd because the storage class specifier applies to the declared variable. It is the declared variable that is static, not the type of the declared variable. Why are e and i ill-formed, but k is well-formed?

What are the rules that govern valid placement of storage class specifiers? While I've used static in this example, the question applies to all storage class specifiers. Preferably, a complete answer should cite relevant sections of the C++11 language standard and explain them.

Diacetylmorphine answered 25/10, 2012 at 17:24 Comment(2)
Why are e and i illformed? They are declaring a static pointer. Pointers can have static lifetime.Corporal
@ThomasMatthews: That's what I'm asking. :-) Visual C++ 2012, Clang 3.1, and g++ 4.7 all accept and reject the same set of declarations (e and i are rejected; the rest are accepted).Diacetylmorphine
E
18

In summary, anywhere in the declaration specifier (See section 7.1 in the ISO/IEC 14882-2012), ie before the *. Qualifiers after the * are associated with the pointer declarator, not the type specifier, and static doesn't make sense within the context of a pointer declarator.

Consider the following cases: You can declare a normal int and a pointer to an int in the same declaration list, like this:

int a, *b;

this is because the type specifier is int, then you have two declarations using that type specifier int, a, and a pointer declarator *a which declares a pointer to int. Now consider:

int a, static b;  // error
int a, *static b; // error
int a, static *b; // error

which should look wrong (as they are), and the reason (as defined in sections 7.1 and 8.1) is because C and C++ require that your storage specifiers go with your type specifier, not in your declarator. So now it should be clear that that the following is also wrong, since the above three are also wrong:

int *static a; // error

Your last example,

typedef int* pointer;
static pointer j;    // valid
pointer static k;    // valid

are both valid and both equivalent because the pointer type is defined as a type specifier and you can put your type specifier and storage specifeir in any order. Note that they are both equivalent and would be equivalent to saying

static int *j;
static int *k;

or

int static *j;
int static *k;
Engstrom answered 25/10, 2012 at 17:38 Comment(3)
+1 for the nice explanation. But I find that the syntactic decision to make "storage specifiers go with the type specifiers, not with the declarators" is really counter-intuitive.Unsophisticated
Thank you, hexist. It occurs to me that I didn't realize this because of two habits I have developed: I never declare more than one variable in a single declaration, and I always stick the pointer asterisk adjacent to the type. Thus, it completely slipped my mind that the asterisk is part of the declarator.Diacetylmorphine
@Unsophisticated it's a straightforward choice, in the context of the "golden rule". I've given an answer that may bring you back intuitivity.Foy
W
5

Per 7.1, the [simplified] structure of C++ declaration is

decl-specifier-seq init-declarator-list;

Per 7.1/1, storage class specifiers belong in the initial "common" part decl-specifier-seq.

Per 8/1, init-declarator-list is a sequence of declarators.

Per 8/4, the * part of pointer declaration is a part of an individual declarator in that sequence. This immediately means that everything that follows a * is a part of that individual declarator. This is why some of your storage class specifier placements are invalid. Declarator syntax does not allow inclusion of storage class specifiers.

The rationale is rather obvious: since storage class specifiers are supposed to apply to all declarators in the whole declaration, they are placed into the "common" part of the declaration.


I'd say that a more interesting (and somewhat related) situation takes place with specifiers that can be present in both decl-specifier-seq and individual declarators, like const specifier. For example, in the following declaration

int const *a, *b;

does const apply to all declarators or only to the first one? The grammar dictates the former interpretation: that const applies to all declarators, i.e. it is a part of the decl-specifier-seq.

Whiteman answered 25/10, 2012 at 21:32 Comment(2)
Thanks a lot, Andrey. You are right, it is rather obvious. As I noted above just now, it ocurs to me that I didn't realize this because of two habits I've developed: I never declare more than one variable in a declaration, and I always stick the pointer asterisk adjacent to the type (not the declared variable). I didn't even think of this. I didn't even think of this when trying to muddle through the spec :-)Diacetylmorphine
in multiple declaration, we still must not forget we can get constness for individual variables, although being unable to get individual multiple decl-specifier-seq at once. int const *a, *const b; = validFoy
A
4

If you employ the "Golden Rule" (which also doesn't apply only to pointers) it follows naturally, intuitively, and it avoids a lot of mistakes and pitfalls when declaring variables in C/C++. The "Golden Rule" should not be violated (there are rare exceptions, like const applied to array typedefs, which propagates const to the base type, and references, that came with C++).

K&R, Appendix A, Section 8.4, Meaning of Declarators states:

Each declarator is taken to be an assertion that when a construction of the same form as the declarator appears in an expression, it yields an object of the indicated type and storage class.

To declare a variable in C/C++ you should really think of the expression you should apply to it to get the base type.

1) There should be a variable name

2) Then comes the expression as valid* out of the declaration statement, applied to the variable name

3) Then comes the remaining information and properties of declaration like base type and storage

Storage is not a characteristic you can always confer to the outcome of expressions, contrary to constness for example. It makes sense only at declaration. So storage must come somewhere else that's not in 2.

int * const *pp;
/*valid*/

int * static *pp;
/*invalid, this clearly shows how storage makes no sense for 2 and so breaks   */
/*the golden rule.                                                             */
/*It's not a piece of information that goes well in the middle of a expression.*/
/*Neither it's a constraint the way const is, it just tells the storage of     */
/*what's being declared.                                                       */

I think K&R wanted us to use inverted reasoning when declaring variables, it's frequently not the common habit. When used, it avoids most of complex declaration mistakes and difficulties.

*valid is not in a strict sense, as some variations occur, like x[], x[size, not indexing], constness, etc... So 2 is a expression that maps well (for the declaration usage), "same form", one that reflects variable's use, but not strictly.

Golden Rule Bonus for the Uninitiated

#include <iostream>

int (&f())[3] {
    static int m[3] = {1, 2, 3};
    return m;
}

int main() {
    for(int i = 0; i < sizeof(f()) / sizeof(f()[0]); ++i)
        std::cout << f()[i] << std::endl;

    return 0;
}

In the context of declarations, & is not an operation to get an address, it just tells what's a reference.

  • f(): f is a function
  • &return: its return is a reference
  • reference[3]: the reference is to an array of 3 elements
  • int array[i]: an element is an int

So you have a function that returns a reference to an array of 3 integers, and as we have the proper compile time information of the array size, we can check it with sizeof anytime =)

Final golden tip, for anything that can be placed before the type, when in multiple declarations, it's to be applied to all the variables at once, and so can't be applied individually.

This const can't be put before int:

int * const p;

So the following is valid:

int * const p1, * const p2;

This one can:

int const *p; // or const int *p;

So the following is invalid:

int const *p1, const *p2;

The exchangeable const is to be applied for all:

int const *p1, *p2; // or const int *p1, *p2;

Declaration Conventions

Because of that, I always put everything that can't be put before the type, closer to the variable (int *a, int &b), and anything that can be put before, I put before (volatile int c).

There's much more on this topic at http://nosubstance.me/post/constant-bikeshedding/.

Avelin answered 25/10, 2012 at 17:25 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.