Вопрос или проблема
C23 §6.2.1(7) содержит новую формулировку, связанную с неопределенными объявлениями (выделено жирным шрифтом ниже):
Теги структур, объединений и перечислений имеют область видимости, которая начинается сразу после появления тега в спецификаторе типа, который объявляет тег. Каждая константа перечисления имеет область видимости, которая начинается сразу после появления ее определяющего перечислителя в списке перечислителей. Обычный идентификатор, имеющий неопределенное определение, имеет область видимости, которая начинается, когда определение завершено; если тот же обычный идентификатор объявляет другую сущность с областью видимости, охватывающей текущий блок, это объявление скрывается, как только внутренний декларатор завершен.[сноска 19] Любой другой идентификатор имеет область видимости, которая начинается сразу после завершения его декларатора.
Сноска 19 гласит:
Это означает, что внешнее объявление не видно для инициатора.
Неопределенное объявление — это объявление, в котором отсутствуют спецификаторы типов или которое объявляется с помощью constexpr
(C23 §6.7.1(12)).
Мне было интересно, почему область видимости неопределенного объявления начинается после завершения его определения, а не после завершения его декларатора, как это происходит для обычных объявлений, которые не неопределены. Согласно N2952, это правило требуется для избежания “цикла в зависимости типов или значений” (см. второй пункт списка 3. Общие синтаксические требования и предложенное решение в 5.2 Область видимости идентификаторов).
Вопрос 1 – Что подразумевается в статье под “циклом в зависимости типов или значений”?
Я думаю, что понимаю проблему цикла в типовой зависимости: если неопределенное объявление не имеет спецификатора типа, то его тип должен выводиться из выражения инициатора. Если инициатор содержит ссылки на неопределенное объявление, например, auto foo = foo * 2;
, то мы получаем циклическую зависимость типов, где тип неопределенного объявления зависит от типа выражения инициатора, который, в свою очередь, зависит от типа неопределенного объявления из-за ссылок. Это делает невозможным вывод типа неопределенного объявления.
Но в чем заключается проблема цикла в значенческой зависимости? Я предполагаю, что это связано с объявлениями constexpr
. В случае обычных объявлений, которые не являются неопределенными, область идентификатора начинается сразу после завершения декларатора, что позволяет свободно использовать его в инициаторе, например, int bar = bar * 2;
. Это нормально, потому что выражение инициатора, bar * 2
, вычисляется во время выполнения, и правила длительности хранения обеспечивают, что bar
существует в памяти к моменту вычисления инициатора. Однако, в случае объявления constexpr
, например, constexpr int baz = baz * 2;
, инициатор должен быть вычислен на этапе компиляции. Но baz
еще не существует на этапе компиляции, что делает невозможным для компилятора вычислить инициатор.
Вопрос 2 – Почему внешние объявления скрываются, как только декларатор завершен?
Согласно приведенному выше отрывку, неопределенное объявление скрывает внешние объявления, как только его декларатор завершен. Это означает, что неопределенное объявление начинает затенять внешние объявления до того, как оно становится видимым (при завершении его определения). Напротив, не неопределенные обычные объявления начинают затенять внешние объявления одновременно с тем, как они становятся видимыми (при завершении их декларатора). Почему неопределенные объявления не действуют так же?
Вопрос 3 – В каком смысле объявления constexpr
являются “неопределенными”?
По определению, существует два типа неопределенных объявлений: 1) те, которые не имеют спецификатора типа, и 2) те, которые объявлены с помощью constexpr
. Это означает, что constexpr
объявление, содержащее спецификатор типа, все еще является “неопределенным”, но в каком смысле? Мое предположение, что термин “неопределенный” был расширен, чтобы включать такие объявления, чтобы отразить тот факт, что у них есть аналогичные синтаксические требования к первому виду неопределенного объявления, то есть к тем, которые не имеют информации о типе (аналогично тому, как typedef
на самом деле не является спецификатором класса хранения, но классифицируется как таковой в грамматике для синтаксического удобства). Это правильно?
Ответ или решение
Изучение программы C23 и вопросов, касающихся нечетко определенных деклараций, открывает интересное и сложное поле для аналитики. Основные моменты в этом контексте включают обсуждение сферы действия нечетко определенных деклараций, проблемы циклической зависимости и шифрования внешних деклараций. В данном ответе я подробно отвечу на поставленные вопросы, основываясь на приведенных данных.
Вопрос 1: Что подразумевает под «циклом зависимости типов или значений»?
Цикл зависимости типов возникает, когда нечетко определенная декларация пытается вывести свой тип, основываясь на инициализирующем выражении, которое в свою очередь ссылается на саму нечетко определенную декларацию. Например, выражение auto foo = foo * 2;
требует от компилятора определить тип foo
на основе его инициализации, но это невозможно, поскольку тип foo
зависит от значения, которое еще не было закомпоновано в момент анализа.
Цикл зависимости значений касается ситуации, когда переменная или константа будет использована в своем собственном инициализирующем выражении до того, как ее значение будет определено в процессе компиляции. Например, в случае с constexpr
, значения должны быть вычислены во время компиляции. Поэтому конструкция constexpr int baz = baz * 2;
приведет к ошибке, поскольку baz
не существует в момент его использования для определения самого себя. Таким образом, наличие циклических зависимостей может препятствовать компилятору правильно обрабатывать и интерпретировать код.
Вопрос 2: Почему внешние декларации скрываются сразу после завершения декларатора?
Отличие в обработке нечетко определенных деклараций, в отличие от обычных деклараций, связано с тем, что нечетко определенные декларации вводят дополнительные сложности в определение области видимости. Когда декларация завершается, компилятору следует предотвратить любые потенциальные конфликты или неожиданное поведение, которые могут возникнуть, если внешняя декларация будет доступна одновременно с замыкающей. Из-за этой особенности нечетко определенные декларации начинают скрывать внешние декларации сразу после завершения.
Это позволяет яснее структурировать код и уменьшить риски возникновения ошибок, а также подразумевает, что логика программирования должна ориентироваться на конструкцию кода, а не на порядок видимости.
Вопрос 3: В каком смысле декларации constexpr
являются «нечетко определенными»?
Определение нечетко определенных деклараций включает как декларации без спецификаторов типов, так и декларации с constexpr
. Даже когда указаны спецификаторы типов, такие декларации создают условия, при которых их значения должны быть сопоставлены или определены во время компиляции. Поскольку они требуют разрешения зависимости значений и типов, аналогично ситуациям, возникающим при отсутствии спецификатора типа, сделанный перенос стал полезным для единообразия и для устранения путаницы в правилах программирования.
В итоговой оценке, расширение определения нечетко определённых деклараций для включения constexpr
позволяет подчеркнуть необходимость строгого учета времени компиляции, предотвращая недоразумения и усложнения в коде.
Заключение
Понимание особенностей нечетко определенных деклараций в стандартном языке C23 критически важно для разработчиков, чтобы избежать распространенных ошибок проектирования. Необходимо учитывать циклы зависимостей, области видимости и особенности работы constexpr
, чтобы писать более надежный и понятный код. Обсуждаемые аспекты не только важны для теории программирования, но и имеют практическое значение для безопасного и эффективного написания кода в современных проектах.