Подтвердить что ты не робот

Что такое декларации и деклараторы и как их типы интерпретируются стандартом?

Как именно стандарт определяет, что, например, float (*(*(&e)[10])())[5] объявляет переменную типа "ссылка на массив из 10 указателей на функцию(), возвращающую указатель на массив из 5 float"?

Вдохновленный дискуссией с @DanNissenbaum

4b9b3361

Ответ 1

Я ссылаюсь на стандарт С++ 11 в этом сообщении

Объявления

Объявления того типа, с которым мы связаны, известны как simple-declaration s в грамматике С++, которые относятся к одной из следующих двух форм (§ 7/1):

decl-specifier-seq opt init-declarator-list opt

attribute-specifier-seq decl-specifier-seq opt init-declarator-list;

Атрибут-спецификатор-seq представляет собой последовательность атрибутов ([[something]]) и/или спецификаторы выравнивания (alignas(something)). Поскольку они не влияют на тип объявления, мы можем игнорировать их и вторую из двух вышеупомянутых форм.

Спецификаторы декларации

Итак, первая часть нашей декларации, decl-specifier-seq, состоит из спецификаторов объявления. К ним относятся некоторые вещи, которые мы можем игнорировать, такие как спецификаторы хранения (static, extern и т.д.), Спецификаторы функций (inline и т.д.), Спецификатор friend и т.д. Однако конкретный спецификатор объявления, представляющий для нас интерес, является спецификатором типа, который может включать простые ключевые слова типа (char, int, unsigned и т.д.), Имена определяемых пользователем типы, cv-квалификаторы (const или volatile) и другие, которые нас не волнуют.

Пример. Таким образом, простой пример определения-specifier-seq, который представляет собой только последовательность спецификаторов типов, const int. Другой может быть unsigned int volatile.

Вы можете подумать: "О, так что что-то вроде const volatile int int float const также является объявлением-спецификатором-seq?" Вы были бы правы, что это соответствует правилам грамматики, но семантические правила запрещают такой spec-specifier-seq. Фактически допускается только один спецификатор типа, за исключением определенных комбинаций (например, unsigned с int или const с чем-либо, кроме самого себя) и требуется хотя бы один не-cv-определитель (§7.1.6/2-3).

Быстрый опрос (вам может потребоваться ссылка на стандарт)

  • Является ли const int const допустимой последовательностью описания объявления или нет? Если нет, то это запрещено с помощью синтаксических или семантических правил?

    Недействителен по семантическим правилам! const нельзя комбинировать с самим собой.

  • Является ли unsigned const int допустимой последовательностью описания объявления? Если нет, то это запрещено с помощью синтаксических или семантических правил?

    Действует! Не имеет значения, что const отделяет unsigned от int.

  • Является ли auto const допустимой последовательностью спецификатора декларации? Если нет, то это запрещено с помощью синтаксических или семантических правил?

    Действует! auto - спецификатор объявления, но сменил категорию на С++ 11. Прежде чем он был спецификатором хранилища (например, static), но теперь это спецификатор типа.

  • Является ли int * const допустимой последовательностью описания объявления? Если нет, то это запрещено с помощью синтаксических или семантических правил?

    Недействителен по синтаксическим правилам! Хотя это вполне может быть полным типом объявления, только int является последовательностью описания объявления. Спецификаторы декларации предоставляют только базовый тип, а не составные модификаторы, такие как указатели, ссылки, массивы и т.д.

Declarators

Вторая часть простой декларации - это список init-declarator. Это последовательность деклараторов, разделенных запятыми, каждая из которых имеет необязательный инициализатор (§8). Каждый декларатор вводит в программу одну переменную или функцию. Самая простая форма декларатора - это просто имя, которое вы вводите - идентификатор декларатора. Объявление int x, y = 5; имеет последовательность спецификатора декларации, которая просто int, за которой следуют два объявления, x и y, второй из которых имеет инициализатор. Однако мы будем игнорировать инициализаторы для остальной части этого сообщения.

В деклараторе может быть особенно сложный синтаксис, поскольку это часть объявления, которая позволяет указать, является ли переменная указателем, ссылкой, массивом, указателем на функцию и т.д. Обратите внимание, что все они являются частью декларатора и а не декларации в целом. Именно поэтому int* x, y; не объявляет два указателя - звездочка * является частью декларатора x, а не частью декларатора y. Важным правилом является то, что каждый декларатор должен иметь ровно один идентификатор объявления - имя, которое он объявляет. Остальные правила о действительных деклараторах применяются после определения типа объявления (мы придем к нему позже).

Пример. Простым примером объявления является *const p, который объявляет указатель const для... чего-то. Тип, на который он указывает, задается спецификаторами объявления в его объявлении. Более ужасающим примером является тот, который задан в вопросе (*(*(&e)[10])())[5], который объявляет ссылку на массив указателей на функции, которые возвращают указатели на... снова, конечная часть типа фактически задается спецификаторами объявления.

Вы вряд ли когда-нибудь столкнетесь с такими ужасными деклараторами, но иногда появляются похожие. Это полезный навык, чтобы иметь возможность читать декларацию, подобную той, что есть в вопросе, и это навык, который приходит с практикой. Полезно понять, как стандарт интерпретирует тип объявления.

Быстрый опрос (вам может потребоваться ссылка на стандарт)

  • Какие части int const unsigned* const array[50]; являются спецификаторами объявления и декларатором?

    Спецификаторы декларации: int const unsigned
     Декларатор: * const array[50]

  • Какие части volatile char (*fp)(float const), &r = c; являются спецификаторами объявления и деклараторами?

    Спецификаторы декларации: volatile char
     Декларатор №1: (*fp)(float const)
     Объявление # 2: &r

Типы деклараций

Теперь мы знаем, что объявление состоит из последовательности спецификаторов декларатора и списка деклараторов, мы можем начать думать о том, как определяется тип объявления. Например, может быть очевидно, что int* p; определяет p как "указатель на int", но для других типов это не так очевидно.

Объявление с несколькими деклараторами, скажем 2 декларатора, считается двумя объявлениями определенных идентификаторов. То есть int x, *y; представляет собой объявление идентификатора x, int x и объявление идентификатора y, int *y.

Типы выражаются в стандарте как англоязычные предложения (такие как "указатель на int" ). Интерпретация типа объявления в этой англоязычной форме выполняется в двух частях. Сначала определяется тип спецификатора декларации. Во-вторых, рекурсивная процедура применяется к объявлению в целом.

Тип спецификаторов декларации

Тип последовательности спецификатора декларации определяется таблицей 10 стандарта. В нем перечислены типы последовательностей, которые содержат соответствующие спецификаторы в любом порядке. Так, например, любая последовательность, содержащая signed и char в любом порядке, включая char signed, имеет тип "подписанный char". Любой cv-квалификатор, который появляется в последовательности спецификатора объявления, добавляется к фронту типа. Итак, char const signed имеет тип "const signed char". Это гарантирует, что независимо от того, в каком порядке вы укажете спецификаторы, тип будет таким же.

Быстрый опрос (вам может потребоваться ссылка на стандарт)

  • Каков тип последовательности описания объявления int long const unsigned?

    "const unsigned long int"

  • Каков тип последовательности описания объявления char volatile?

    "volatile char"

  • Каков тип последовательности описания объявления auto const?

    Это зависит! auto будет выводиться из инициализатора. Если его вывести как int, например, тип будет "const int".

Тип декларации

Теперь, когда у нас есть тип последовательности описания объявления, мы можем выработать тип целого объявления идентификатора. Это делается путем применения рекурсивной процедуры, определенной в разделе 8.3. Чтобы объяснить эту процедуру, я воспользуюсь примером. Мы разработаем тип e в float const (*(*(&e)[10])())[5].

Шаг 1Первым шагом является разделение объявления на форму T D, где T - это спецификатор объявления, а D - декларатор. Итак, мы получаем:

T = float const
D = (*(*(&e)[10])())[5]

Тип T - это, конечно, "const float", как мы определили в предыдущем разделе. Затем мы ищем подраздел § 8.3, который соответствует текущей форме D. Вы обнаружите, что это §8.3.4 Массивы, поскольку он утверждает, что он применяется к объявлениям формы T D, где D имеет следующий вид:

D1 [ константа-выражение opt] attribute-specifier-seq opt

Наш D действительно имеет ту форму, где D1 есть (*(*(&e)[10])()).

Теперь представьте себе объявление T D1 (мы избавились от [5]).

T D1 = const float (*(*(&e)[10])())

Это тип "< some stuff > T" . В этом разделе указывается, что тип нашего идентификатора e - это " > массив элементов > 5 из T", где < some stuff > такая же, как и в типе мнимой декларации. Поэтому, чтобы выработать оставшуюся часть типа, нам нужно определить тип T D1.

Это рекурсия! Мы рекурсивно разрабатываем тип внутренней части декларации, отбрасывая ее на каждом шагу.

Шаг 2 Итак, как и раньше, мы разделили наше новое объявление на форму T D:

T = const float
D = (*(*(&e)[10])())

Это соответствует пункту §8.3/6, где D имеет вид ( D1 ). Этот случай прост, тип T D - это просто тип T D1:

T D1 = const float *(*(&e)[10])()

Шаг 3 Теперь позвоните в этот T D и разделите его снова:

T = const float
D = *(*(&e)[10])()

Это соответствует §8.3.1 Указатели, где D имеет вид * D1. Если T D1 имеет тип "< some stuff > T" , то T D имеет тип "< some stuff > указатель на T". Итак, теперь нам нужен тип T D1:

T D1 = const float (*(&e)[10])()

Шаг 4 Мы называем его T D и разделяем его:

T = const float
D = (*(&e)[10])()

Это соответствует §8.3.5. Функции, где D имеет вид D1 (). Если T D1 имеет тип "< some stuff > T" , то T D имеет тип " > function > function of(), возвращающий T". Итак, теперь нам нужен тип T D1:

T D1 = const float (*(&e)[10])

Шаг 5. Мы можем применить то же правило, которое мы сделали для шага 2, где декларатор просто заключен в скобки:

T D1 = const float *(&e)[10]

Шаг 6 Конечно, мы разделили его:

T = const float
D = *(&e)[10]

Мы снова вернемся к §8.3.1. Указатели снова с D формы * D1. Если T D1 имеет тип "< some stuff > T" , то T D имеет тип "< some stuff > указатель на T". Итак, теперь нам нужен тип T D1:

T D1 = const float (&e)[10]

Шаг 7 Разделить его:

T = const float
D = (&e)[10]

Мы снова вернемся к §8.3.4 Массивы с D формы D1 [10]. Если T D1 имеет тип "< some stuff > T" , то T D имеет тип "< some stuff > array of 10 T". Итак, что такое T D1 type?

T D1 = const float (&e)

Шаг 8 Повторно примените шаг круглых скобок:

T D1 = const float &e

Шаг 9 Разделить его:

T = const float
D = &e

Теперь мы сопоставим §8.3.2. Ссылки, где D имеет вид & D1. Если T D1 имеет тип "< some stuff > T" , то T D имеет тип "< some stuff > reference to T". Итак, каков тип T D1?

T D1 = const float e

Шаг 10 Ну, это просто "Т", конечно! Нет никакого материала > на этом уровне. Это дается правилом базового случая в §8.3/5.

И мы закончили!

Итак, теперь, если мы посмотрим на тип, который мы определили на каждом шаге, подставив значение типа > s с каждого уровня ниже, мы можем определить тип e в float const (*(*(&e)[10])())[5]:

<some stuff> array of 5 T
│          └──────────┐
<some stuff> pointer to T
│          └────────────────────────┐
<some stuff> function of () returning T
|          └──────────┐
<some stuff> pointer to T
|          └───────────┐
<some stuff> array of 10 T
|          └────────────┐
<some stuff> reference to T
|          |
<some stuff> T

Если мы объединим это все вместе, то получим:

reference to array of 10 pointer to function of () returning pointer to array of 5 const float

Ницца! Таким образом, показано, как компилятор выводит тип объявления. Помните, что это применяется к каждому объявлению идентификатора, если имеется несколько деклараторов. Попытайтесь выяснить их:

Быстрый опрос (вам может потребоваться ссылка на стандарт)

  • Каков тип x в объявлении bool **(*x)[123];?

    "указатель на массив из 123 указателя на указатель на bool"

  • Каковы типы y и z в объявлении int const signed *(*y)(int), &z = i;?

    y является "указателем на функцию возвращаемого указателя (int) на const signed int"  z является "ссылкой на const signed int"

Если у кого-либо есть какие-либо исправления, пожалуйста, дайте мне знать!

Ответ 2

Вот как я разбираю float const (*(*(&e)[10])())[5]. Прежде всего, определите спецификатор. Здесь спецификатор float const. Теперь посмотрим на приоритет. [] = () > *. Круглые скобки используются для устранения неоднозначности приоритета. Имея в виду приоритет, пусть идентифицирует идентификатор переменной, который равен e. Таким образом, e является ссылкой на массив (так как [] > *) из 10 указателей на функции (поскольку () > *), которые не принимают аргумент и return и указатель на массив из 5 float const. Таким образом, спецификатор приходит последним, и остальные анализируются в соответствии с приоритетом.