Как именно стандарт определяет, что, например, 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. Таким образом, спецификатор приходит последним, и остальные анализируются в соответствии с приоритетом.