Как именно стандарт определяет, что, например, float (*(*(&e)[10])())[5] объявляет переменную типа "ссылка на массив из 10 указателей на функцию(), возвращающую указатель на массив из 5 float"?
Вдохновленный дискуссией с @DanNissenbaum
Как именно стандарт определяет, что, например, float (*(*(&e)[10])())[5] объявляет переменную типа "ссылка на массив из 10 указателей на функцию(), возвращающую указатель на массив из 5 float"?
Вдохновленный дискуссией с @DanNissenbaum
Я ссылаюсь на стандарт С++ 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является последовательностью описания объявления. Спецификаторы декларации предоставляют только базовый тип, а не составные модификаторы, такие как указатели, ссылки, массивы и т.д.
Вторая часть простой декларации - это список 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"
Если у кого-либо есть какие-либо исправления, пожалуйста, дайте мне знать!
Вот как я разбираю float const (*(*(&e)[10])())[5]. Прежде всего, определите спецификатор. Здесь спецификатор float const. Теперь посмотрим на приоритет. [] = () > *. Круглые скобки используются для устранения неоднозначности приоритета. Имея в виду приоритет, пусть идентифицирует идентификатор переменной, который равен e. Таким образом, e является ссылкой на массив (так как [] > *) из 10 указателей на функции (поскольку () > *), которые не принимают аргумент и return и указатель на массив из 5 float const. Таким образом, спецификатор приходит последним, и остальные анализируются в соответствии с приоритетом.