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

Почему 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'
4b9b3361

Ответ 1

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

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

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

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

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

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

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

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

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

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

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

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

Ответ 2

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

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, поскольку он дает понять, что определенный линкером символ не имеет собственного "типа".

Ответ 3

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

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

От N1570

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

J2. Undefined Поведение

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

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

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

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

и

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

Ограничения

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

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

6.3.2.2 void

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

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

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

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

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

Ответ 4

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

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

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