Где в объявлении может быть указан спецификатор класса хранения?

Например, рассмотрим спецификатор класса static хранения. Вот несколько примеров действительного и плохо сформированного использования этого спецификатора класса хранения:

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

(Декларации, помеченные как "действительные", были приняты Visual С++ 2012, g++ 4.7.2 и Clang++ 3.1. Объявления, помеченные как "плохо сформированные", были отклонены всеми этими компиляторами.)

Это кажется странным, потому что спецификатор класса хранения применяется к объявленной переменной. Это объявленная переменная static, а не тип объявленной переменной. Почему e и i плохо сформированы, но k хорошо сформирован?

Каковы правила, которые определяют правильное размещение спецификаторов класса хранения? Хотя в этом примере я использовал static, этот вопрос относится ко всем спецификациям класса хранения. Предпочтительно, полный ответ должен привести соответствующие разделы стандарта языка С++ 11 и объяснить их.

Ответ 1

В целом, где угодно в спецификаторе декларации (см. раздел 7.1 в ИСО/МЭК 14882-2012), т.е. до *. Квалификаторы после * связаны с объявлением указателя, а не с спецификатором типа, и static не имеет смысла в контексте декларатора указателя.

Рассмотрим следующие случаи: Вы можете объявить нормальный int и указатель на int в том же списке объявлений, как это:

int a, *b;

это потому, что спецификатор типа int, тогда у вас есть два объявления с использованием этого спецификатора типа int, a и декларатора указателя *a, который объявляет указатель на int. Теперь рассмотрим:

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

которые должны выглядеть не так (как они есть), и причина (как определено в разделах 7.1 и 8.1) заключается в том, что C и С++ требуют, чтобы ваши спецификаторы хранилища шли с вашим спецификатором типа, а не в вашем деклараторе. Итак, теперь должно быть ясно, что следующее неверно, так как вышеупомянутые три также неверны:

int *static a; // error

В последнем примере

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

оба действительны и оба эквивалентны, потому что тип pointer определяется как спецификатор типа, и вы можете поместить свой спецификатор типа и хранилище specifeir в любом порядке. Обратите внимание, что они оба эквивалентны и будут эквивалентны выражению

static int *j;
static int *k;

или

int static *j;
int static *k;

Ответ 2

Если вы используете "Золотое правило" (что также не относится только к указателям), это естественно, интуитивно, и это избегает много ошибок и ловушек при объявлении переменных в C/С++. "Золотое правило" не должно быть нарушено (есть редкие исключения, например const, применяемые к массивам typedefs, которые распространяют const на базовый тип и ссылки, которые поставляются с С++).

K & R, Приложение A, Раздел 8.4, Значение Declarators:

Каждый декларатор считается утверждением, что при построении той же формы, что и декларатор в выражении, он дает объект указанного типа и класс хранения.

Чтобы объявить переменную в C/С++, вы действительно должны думать о выражении, которое вы должны применить к нему, чтобы получить базовый тип.

1) Должно быть имя переменной

2) Затем приходит выражение как действительное * из инструкции объявления, применяемое к имени переменной

3) Затем появляется оставшаяся информация и свойства объявления типа базового типа и хранилища

Хранение не является характеристикой, которую вы всегда можете отнести к результату выражений, например, в противоположность константе. Это имеет смысл только при объявлении. Поэтому хранилище должно прибывать куда-то еще, а не в 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 not a piece of information that goes well in the middle of a expression.*/
/*Neither it a constraint the way const is, it just tells the storage of     */
/*what being declared.                                                       */

Я думаю, что K & R хотел, чтобы мы использовали инвертированные рассуждения при объявлении переменных, это часто не обычная привычка. При использовании он избегает большинства сложных ошибок и трудностей декларирования.

* valid не в строгом смысле, поскольку некоторые изменения происходят, как x [], x [размер, не индексирование], constness и т.д. Итак, 2 - это выражение, которое хорошо отображает (для использования декларации), "та же форма", которая отражает использование переменной, но not строго.

Бонус Золотого правила для непосвященных

#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;
}

Чтение & в декларациях работает почти так же, как чтение const (& не может быть помещено перед типом). В этом контексте это не операция для получения адреса и может быть визуализирована как новое ключевое слово, например ref или reference.

  • f(): f является функцией
  • & return: его return - это ссылка
  • ссылка [3]: ссылка относится к массиву из 3 элементов
  • int массив [i]: элемент - это int

Итак, у вас есть функция, которая возвращает ссылку на массив из 3 целых чисел, и поскольку у нас есть правильная информация о времени компиляции массива, мы можем проверить ее с помощью sizeof anytime =)

Конечный золотой наконечник для всего, что может быть помещено перед типом, когда в нескольких объявлениях оно применяется ко всем переменным одновременно и поэтому не может применяться индивидуально.

Этот const нельзя поставить перед int:

int * const p;

Итак, справедливо следующее:

int * const p1, * const p2;

Это может быть:

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

Таким образом, следующее недопустимо:

int const *p1, const *p2;

Сменный const должен применяться для всех:

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

Соглашения об условных обозначениях

Из-за этого я всегда ставил все, что не может быть поставлено перед типом, ближе к переменной (int *a, int &b) и все, что может, прежде чем поставить (volatile int c). Если бы я мог приклеить *const к имени переменной, например, с помощью *&, я бы сделал это.

Ответ 3

Per 7.1, [упрощенная] структура объявления С++

decl-specifier-seq init-declarator-list;

В соответствии с 7.1/1 спецификаторы класса хранения относятся к начальной "общей" части decl-specifier-seq.

Per 8/1, init-declarator-list - это последовательность деклараторов.

В 8/4 часть * декларации указателя является частью отдельного декларатора в этой последовательности. Это немедленно означает, что все, что следует за *, является частью этого индивидуального декларатора. Вот почему некоторые места размещения спецификаторов класса хранилища недействительны. Синтаксис Declarator не позволяет включать спецификаторы класса хранения.

Обоснование довольно очевидно: поскольку спецификаторы класса хранения должны применяться ко всем деклараторам во всей декларации, они помещаются в "общую" часть декларации.


Я бы сказал, что более интересная (и несколько связанная) ситуация имеет место с спецификаторами, которые могут присутствовать как в decl-specifier-seq, так и в отдельных деклараторах, таких как спецификатор const. Например, в следующем объявлении

int const *a, *b;

делает const применимо ко всем деклараторам или только к первому? Грамматика диктует прежнюю интерпретацию: const применяется ко всем деклараторам, т.е. Является частью decl-specifier-seq.