Почему gcc разрешает объявления extern типа void (non-pointer)?

Почему gcc разрешает объявления extern типа void? Является ли это расширением или стандартом C? Существуют ли для этого приемлемые применения?

Я предполагаю, что это расширение, но я не нахожу его упомянутым в:
http://gcc.gnu.org/onlinedocs/gcc-4.3.6/gcc/C-Extensions.html

$ cat extern_void.c extern void foo; /* ok in gcc 4.3, not ok in Visual Studio 2008 */ void* get_foo_ptr(void) { return &foo; } $ gcc -c extern_void.c # no compile error $ gcc --version | head -n 1 gcc (Debian 4.3.2-1.1) 4.3.2 

Определение foo как типа void – это, конечно, ошибка компиляции:

 $ gcc -c -Dextern= extern_void.c extern_void.c:1: error: storage size of 'foo' isn't known 

Для сравнения, Visual Studio 2008 дает ошибку в объявлении extern:

 $ cl /c extern_void.c Microsoft (R) 32-bit C/C++ Optimizing Compiler Version 15.00.21022.08 for 80x86 Copyright (C) Microsoft Corporation. All rights reserved. extern_void.c extern_void.c(1) : error C2182: 'foo' : illegal use of type 'void' 

Как ни странно (или, может быть, не так странно …) мне кажется, что gcc правильно принять это.

Если бы это было объявлено static а не extern , тогда у него была бы внутренняя связь, и применим §6.9.2 / 3:

Если объявление идентификатора для объекта является предварительным определением и имеет внутреннюю связь, объявленный тип не должен быть неполным.

Если в этом случае не указывается class хранения ( extern , в данном случае), то применяется §6.7 / 7:

Если идентификатор для объекта объявлен без привязки, тип объекта должен быть завершен до конца его декларатора или к концу его инициатора инициализации, если он имеет инициализатор; в случае аргументов функции (в том числе в прототипах), это скорректированный тип (см. 6.7.5.3), который должен быть полным.

Я либо из этих случаев, void не будет работать, потому что (§6.2.5 / 19):

Тип void […] – неполный тип, который не может быть завершен.

Однако ни одно из них не применяется. Это, кажется, оставляет только требования §6.7.2 / 2, которые, по-видимому, позволяют объявлять имя с типом void :

По меньшей мере один спецификатор типа должен указываться в спецификаторах объявления в каждом объявлении и в списке спецификаторов-спецификаторов в каждом объявлении структуры и имени типа. Каждый список спецификаторов типов должен быть одним из следующих наборов (разделенных запятыми, когда на линии имеется более одного набора); спецификаторы типов могут встречаться в любом порядке, возможно, смешанные с другими спецификаторами объявления.

  • недействительным
  • голец
  • подписанный символ

[… больше типов elided]

Я не уверен, что это действительно преднамеренно – я подозреваю, что void действительно предназначен для таких вещей, как производные типы (например, указатель на void) или тип возврата из функции, но я не могу найти ничего, что прямо указывает на это ограничение.

Я нашел единственное законное использование для объявления

 extern void foo; 

когда foo является символом ссылки (внешним символом, определенным компоновщиком), который обозначает адрес объекта неопределенного типа.

Это действительно полезно, потому что символы ссылки часто используются для связи объема памяти; т.е. начальный адрес раздела текста, длина раздела текста и т. д.

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

 uint32_t textLen; textLen = ( uint32_t )foo; 

Или, если foo – начальный адрес той же области памяти:

 uint8_t *textStart; textStart = ( uint8_t * )foo; 

Единственный альтернативный способ ссылки на символ ссылки в «С», который я знаю, – объявить его как внешний массив:

 extern uint8_t foo[]; 

Я на самом деле предпочитаю объявление void , поскольку он дает понять, что определенный линкером символ не имеет собственного «типа».

GCC (также, LLVM C frontend) определенно глючит. Как Комо, так и MS, похоже, сообщают об ошибках.

Фрагмент OP имеет по крайней мере два определенных UB и одну красную селедку:

От N1570

[UB # 1] Отсутствует main в размещенной среде:

J2. Неопределенное поведение

[…] Программа в размещенной среде не определяет функцию с именем main, используя одну из указанных форм (5.1.2.2.1).

[UB # 2] Даже если мы проигнорируем вышеизложенное, по-прежнему остается проблема принятия адреса явно выраженного выражения void :

6.3.2.1 Lvalues, массивы и указатели функций

1 lvalue – выражение (с типом объекта, отличным от void), который потенциально обозначает объект; 64)

а также:

6.5.3.2 Операторы адреса и косвенности

Ограничения

Оператор унарного оператора 1Т должен быть либо обозначением функции, результатом оператора [], либо унарного *, либо значением l, которое обозначает объект, который не является битовым полем и не объявлен с classом хранения регистров спецификатор.

[Примечание: акцент на шахту lvalue ] Также есть раздел в стандарте, конкретно void :

6.3.2.2 void

1 (несуществующее) значение выражения void (выражение, которое имеет тип void) никоим образом не должно использоваться, а неявные или явные преобразования (кроме void) не должны применяться к такому выражению.

Определение файловой области – это первичное выражение (6.5). Итак, берет адрес объекта, обозначенного foo . BTW, последний вызывает UB. Таким образом, это явно исключается. То, что еще предстоит выяснить, заключается в том, что удаление квалификатора extern делает вышеизложенное действительным или нет:

В нашем случае для foo согласно п. 6.2.2 / 5:

5 […] Если объявление идентификатора для объекта имеет область действия файла и спецификатор classа хранения, его связь является внешней.

т.е. даже если бы мы оставили вне extern мы все равно приземлились бы в одной и той же проблеме.

Одним из ограничений семантики взаимодействия с компоновщиками C является то, что он не предоставляет механизма для предоставления числовых констант времени привязки. В некоторых проектах может потребоваться, чтобы статические инициализаторы включали числовые значения, которые недоступны во время компиляции, но будут доступны во время ссылки. На некоторых платформах это может быть достигнуто путем определения где-нибудь (например, в файле на ассемблере) ярлыка, адрес которого, если он передан в int , даст числовое значение, представляющее интерес. Затем в файле C можно использовать определение extern чтобы сделать «адрес» этой вещи доступной как константу времени компиляции.

Этот подход очень специфичен для платформы (как и любое использование языка ассемблера), но делает возможными некоторые конструкции, которые в противном случае были бы проблематичными. Несколько неприятным аспектом этого является то, что если метка определена в C как тип, такой как unsigned char[] , это будет показывать впечатление, что адрес может быть разыменован или выполнить арифметику. Если компилятор примет void foo; , тогда (int)&foo преобразует назначенный компоновщиком адрес для foo в целое число, используя ту же семантику-указатель-целое, что и применительно к любому другому `void *.

Я не думаю, что когда-либо использовал void для этой цели (я всегда использовал extern unsigned char[] ), но думал бы, что void будет чище, если что-то определит его как законное расширение (ничто в стандарте C не требует, чтобы любая возможность существует где угодно, чтобы создать символ компоновщика, который может использоваться как что-либо иное, кроме одного конкретного невоидного типа; на платформах, где не существовало бы средств для создания идентификатора компоновщика, который программа C могла бы определять как extern void , не было бы необходимо, чтобы компиляторы допускали такой синтаксис).