Повторяющиеся typedefs - недействительны в C, но действительны в C++?

Мне нужна стандартная ссылка, почему следующий код вызывает предупреждение о соответствии в C (проверено с помощью gcc -pedantic; «переопределение typedef»), но отлично подходит для C++ (g++ -pedantic):

typedef struct Foo Foo;
typedef struct Foo Foo;

int main() { return 0; }

Почему я не могу повторно определить typedef в C?

(Это имеет практическое значение для структуры заголовка Проект C.)


person Kerrek SB    schedule 21.12.2011    source источник
comment
Потому что компилятор теряет терпение с вами. Он услышал вас в первый раз. Кроме того, гибкое / прагматичное программирование подходит для DRY (не повторяйтесь) и SPOT (единственная точка истины). Если вы повторяете typedef, вы нарушаете оба.   -  person Jonathan Leffler    schedule 21.12.2011
comment
@JonathanLeffler: Код, который у меня есть, действительно мог бы выиграть от определения типов, хотя и с точки зрения единообразия. Моя текущая работа заключается не в том, чтобы использовать его и писать struct Foo напрямую, но каким-то образом это противоречит стилю кода. (Это связано с созданием экземпляра этого шаблон хеш-таблицы.)   -  person Kerrek SB    schedule 21.12.2011
comment
Возможно, вам следует сделать please typedef struct Foo Foo;, чтобы успокоить компилятор.   -  person R. Martinho Fernandes    schedule 21.12.2011
comment
FWIW, Clang 3.1 отклоняет этот код с ошибкой: t.c:2:20: error: переопределение typedef 'Foo' недопустимо в C [-Wtypedef-redefinition].   -  person Xeo    schedule 21.12.2011
comment
Все ответы полезны и хороши, позвольте мне принять тот, у которого наименьшая репутация.   -  person Kerrek SB    schedule 22.12.2011
comment
FWIW: Стандарт C 2011 года был опубликован ISO в понедельник 19 декабря 2011 года. См. объявление на веб-сайте WG14. К сожалению, PDF из ISO стоит 330 швейцарских франков.   -  person Jonathan Leffler    schedule 22.12.2011
comment
@Jonathan: У меня есть N1548 (датированный декабрем 2010 г.), есть идеи, насколько это близко?   -  person Steve Jessop    schedule 22.12.2011
comment
О, и N1570 все еще доступен, покупайте, пока они горячие. Какой номер проекта был фактически утвержден?   -  person Steve Jessop    schedule 22.12.2011


Ответы (5)


Почему это компилируется в C++?

Потому что стандарт С++ прямо говорит об этом.

Ссылка:

Спецификатор определения типа C++03 Standard 7.1.3

§7.1.3.2:

В заданной области, не относящейся к классу, спецификатор typedef может использоваться для переопределения имени любого типа, объявленного в этой области, для ссылки на тип, на который он уже ссылается.

[Пример:
typedef struct s { /* ... */ } s;
typedef int I;
typedef int I;
typedef II;
—end пример]

Почему это не компилируется в C?

typedef имена не имеют связи, а стандарт C99 запрещает идентификаторам без спецификации связи иметь более одного объявления с одной и той же областью действия и в одном пространстве имен.

Ссылка:

Стандарт C99: §6.2.2 Связи идентификаторов

§6.2.2/6 гласит:

Следующие идентификаторы не имеют связи: идентификатор, объявленный чем-то иным, чем объект или функция; идентификатор, объявленный как параметр функции; идентификатор области блока для объекта, объявленного без спецификатора класса хранения.

Далее в §6.7/3 говорится:

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

person Alok Save    schedule 21.12.2011
comment
Просто чтобы подвести итог другим ответам. Следующая версия C, C11, позволит это и, таким образом, устранит одну из несовместимостей между C++ и C. - person Jens Gustedt; 22.12.2011

Стандарт C теперь называется ISO/IEC 9989:2011.

Стандарт C 2011 был опубликован ISO в понедельник 2011-12-19 (или, точнее, уведомление о том, что он был опубликован, было добавлено на веб-сайт комитета 19 числа; стандарт мог быть опубликован как «давно» по состоянию на 08.12.2011). См. объявление на веб-сайте WG14. К сожалению, PDF из ISO стоит 338 швейцарских франков, < s>и от ANSI 387 долларов США.

  • Вы можете получить PDF-файл INCITS/ISO/IEC 9899:2012 (C2011) по адресу ANSI за 30 долларов США.
  • Вы можете получить PDF-файл для INCITS/ISO/IEC 14882:2012 (C++2011) по адресу ANSI за 30 долларов США.

Основной ответ

Вопрос в том, «Разрешены ли повторные определения типов в C»? Ответ: «Нет, не в стандартах ISO/IEC 9899:1999 или 9899:1990». Причина, вероятно, историческая; первоначальные компиляторы C не позволяли этого, поэтому первоначальные стандартизаторы (которые были уполномочены стандартизировать то, что уже было доступно в компиляторах C) стандартизировали такое поведение.

См. ответ от Als, где стандарт C99 запрещает повторяющиеся определения типов. Стандарт C11 изменил правило в §6.7 ¶3 на:

3 Если идентификатор не имеет связи, должно быть не более одного объявления идентификатора (в описателе или спецификаторе типа) с той же областью действия и в одном и том же пространстве имен, за исключением того, что:

  • имя typedef может быть переопределено для обозначения того же типа, что и в настоящее время, при условии, что этот тип не является изменяемым типом;
  • теги могут быть повторно объявлены, как указано в 6.7.2.3.

Итак, теперь в C11 есть явный мандат на повторение typedef. Обратите внимание на наличие C11-совместимых компиляторов C.


Для тех, кто все еще использует C99 или более раннюю версию, последующий вопрос, предположительно, звучит так: «Итак, как мне избежать проблем с повторяющимися определениями типов?»

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

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

См. также Что такое внешние переменные в C; он говорит о переменных, но с типами можно обращаться примерно так же.


Вопрос из комментариев

Мне очень нужны «неполные объявления структур» из-за отдельных усложнений препроцессора, которые запрещают определенные включения. Итак, вы говорите, что я не должен определять эти предварительные объявления, если они снова определяются полным заголовком?

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

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

Все заголовки являются (а) автономными и (б) идемпотентными. Это означает, что вы можете (а) включить заголовок, и все другие необходимые заголовки будут включены автоматически, и (б) вы можете включить заголовок несколько раз, не навлекая на себя гнев компилятора. Последнее обычно достигается с помощью защиты заголовков, хотя некоторые предпочитают #pragma once, но это не переносимо.

Итак, у вас может быть такой публичный заголовок:

общественность.ч

#ifndef PUBLIC_H_INCLUDED
#define PUBLIC_H_INCLUDED

#include <stddef.h>    // size_t

typedef struct mine mine;
typedef struct that that;

extern size_t polymath(const mine *x, const that *y, int z);

#endif /* PUBLIC_H_INCLUDED */

Пока так не очень спорно (хотя можно с полным основанием подозревать, что интерфейс, предоставляемый этой библиотекой, очень неполный).

приват.ч

#ifndef PRIVATE_H_INCLUDED
#define PRIVATE_H_INCLUDED

#include "public.h"  // Get forward definitions for mine and that types

struct mine { ... };
struct that { ... };

extern mine *m_constructor(int i);
...

#endif /* PRIVATE_H_INCLUDED */

Опять же, не очень спорно. Заголовок public.h должен быть указан первым; это обеспечивает автоматическую проверку автономности.

Потребительский код

Любой код, которому нужны службы polymath(), пишет:

#include "public.h"

Это вся информация, необходимая для использования сервиса.

Код провайдера

Любой код в библиотеке, определяющий службы polymath(), пишет:

#include "private.h"

После этого все работает как обычно.

Другой код провайдера

Если есть другая библиотека (назовем ее multimath()), которая использует службы polymath(), то этот код должен включать public.h, как и любой другой потребитель. Если службы polymath() являются частью внешнего интерфейса для multimath(), то общедоступный заголовок multimath.h будет включать public.h (извините, здесь я поменял терминологию ближе к концу). Если службы multimath() полностью скрывают службы polymath(), то заголовок multimath.h не будет включать public.h, но закрытый заголовок multimath() вполне может это сделать, или отдельные исходные файлы, которым нужны службы polymath(), могут включать его при необходимости.

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

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

Конечно, у вас может быть два заголовка, которые просто несовместимы. Для надуманного примера, если есть (плохо спроектированный) заголовок, который объявляет другую версию структуры FILE (от версии в <stdio.h>), вы попали в шланг; код может включать либо плохо спроектированный заголовок, либо <stdio.h>, но не то и другое одновременно. В этом случае плохо спроектированный заголовок следует пересмотреть, чтобы использовать новое имя (возможно, File, но, возможно, что-то еще). Реально вы можете столкнуться с этой проблемой, если вам нужно объединить код из двух продуктов в один после корпоративного поглощения с некоторыми общими структурами данных, такими как DB_Connection для соединения с базой данных. В отсутствие функции C++ namespace вы застряли в упражнении по переименованию одного или обоих участков кода.

person Jonathan Leffler    schedule 21.12.2011
comment
Да, мне очень нужны неполные объявления структур из-за отдельных усложнений препроцессора, запрещающих некоторые включения. Итак, вы говорите, что я не должен не определять типы этих опережающих объявлений, если они снова определяются полным заголовком? - person Kerrek SB; 21.12.2011
comment
Это в основном не ответ. - person Jens Gustedt; 22.12.2011
comment
@Jens: почему? Существует прямой (но краткий) ответ на прямой вопрос и более длинный, более содержательный ответ, который объясняет, как обойти проблемы, которые, как правило, приводят к желанию задать прямой вопрос в первую очередь. И примечание о стандарте C11 (которое, я полагаю, можно считать не относящимся к теме). - person Jonathan Leffler; 22.12.2011
comment
@JonathanLeffler, начало ваших ответов вообще не имеет отношения к вопросу. Начинать с примечания — не лучшая идея, чтобы кто-то читал дальше. Насколько я помню, ваша точность для нет (не с C99) была добавлена ​​​​после того, как я сделал свой комментарий (и в значительной степени улучшает читаемость). - person Jens Gustedt; 22.12.2011

Вы можете сделать это на С++ из-за 7.1.3/3 и /4.

Вы не можете сделать это в C99, потому что в 6.7.7 нет эквивалентного особого случая, поэтому повторное объявление имени typedef следует тем же правилам, что и повторное объявление любого другого идентификатора. В частности, 6.2.2/6 (у typedefs нет привязки) и 6.7/3 (идентификаторы без привязки могут быть объявлены только один раз с той же областью).

Помните, что typedef — это спецификатор класса хранения в C99, тогда как в C++ это спецификатор decl. Другая грамматика заставляет меня подозревать, что авторы C++ решили приложить больше усилий для того, чтобы сделать typedef «объявлением другого типа», и поэтому, возможно, были готовы потратить больше времени и текста на специальные правила для них. Кроме того, я не знаю, какова была (отсутствие) мотивация авторов C99.

[Редактировать: см. ответ Йоханнеса для C1x. Я совсем не слежу за этим, поэтому мне, вероятно, следует прекратить использовать «C» для обозначения «C99», потому что я, вероятно, даже не замечу, когда они ратифицируют и опубликуют. Это и так достаточно плохо: «C» должно означать «C99», но на практике означает «C99, если вам повезет, но если вам нужно поддерживать MSVC, тогда C89».]

[Редактировать еще раз: и действительно, он был опубликован и теперь является C11. Уот.]

person Steve Jessop    schedule 21.12.2011
comment
Не могли бы вы подробнее рассказать о классе хранения и спецификаторе decl? - person Kerrek SB; 21.12.2011
comment
@Kerrek: Посмотрите на грамматику в соответствующих стандартах: эквивалентом C++ decl-specifier-seq является C declaration-specifiers. Обратите внимание, что в дополнение к вещам, перечисленным в C, C++ допускает использование ключевых слов friend и constexpr, которые также являются особыми типами объявлений. Он перемещает typedef из storage-class-specifier в decl-specifier. Разница ничего не доказывает, это просто другой способ определения грамматики, но она указывает мне на то, что C++ думает, что у нас есть несколько разных типов объявлений, тогда как C думает, что мы можем использовать определения типов с минимальными усилиями. - person Steve Jessop; 21.12.2011

Многие люди ответили, ссылаясь на стандарты, но никто не сказал, ПОЧЕМУ стандарты различаются для C и C++ здесь. Что ж, я полагаю, что причиной разрешения повторяющихся определений типов в C++ было то, что C++ неявно объявляет структуры и классы как типы. Таким образом, в С++ допустимо следующее:

struct foo { int a; int b; };
foo f;

В C нужно написать:

struct foo { int a; int b; };
typedef struct foo foo;
foo f;

Существует много подобного кода на C, в котором структуры объявляются как типы. Если такой код переносится на C++, определения типов дублируются, поскольку язык C++ добавляет свои собственные неявные определения типов. Таким образом, чтобы программисты не заморачивались с удалением тех определений типов, которые больше не требуются, они с самого начала разрешили дублировать определения типов в C++.

Как уже говорили другие, люди со временем поняли, что повторение идентичных определений типов в C также может быть полезным. По крайней мере, не должно навредить. Вот почему эта функция C++ была как бы «перенесена» в C11.

person Kai Petzke    schedule 15.03.2014

В спецификации c нет ничего, что объясняло бы, почему это недопустимо. Спецификация - неподходящее место для разъяснения этого. FWIW это разрешено в C1x (согласно ответу, который я получил на один из моих последних вопросов).

Я предполагаю, что эта функция c1x поддерживает преобразование макросов в typedefs (первые разрешено повторять, если они идентичны).

person Johannes Schaub - litb    schedule 21.12.2011
comment
Интересно! Теперь, когда MS предоставит компилятор, совместимый с C1x, чтобы мы могли его использовать? - person Jonathan Leffler; 21.12.2011
comment
Итак, чтобы уточнить, в C это запрещено по той же причине, что и int x; int x;? - person Kerrek SB; 21.12.2011
comment
@Jonathan Leffler: какова длина бесконечно длинной строки? - person Steve Jessop; 21.12.2011
comment
Боюсь, дольше, чем я готов ждать. :( - person Jonathan Leffler; 22.12.2011
comment
@JonathanLeffler: Не бойтесь, в мире полно поставщиков компиляторов, которые стремятся добиться соответствия C1x. - person Stephen Canon; 22.12.2011
comment
@KerrekSB: int x; int x; является допустимым в C (в области файла), это всего лишь два предварительных определения объекта с внешней связью, довольно близко друг к другу. - person CB Bailey; 22.12.2011
comment
@CharlesBailey: Хм... какой хороший пример чего-то похожего на typedef, что нельзя сделать дважды? [Вы можете присоединиться к чату?] - person Kerrek SB; 22.12.2011
comment
@KerrekSB: int x = 0; int x = 0; - person CB Bailey; 22.12.2011
comment
@JonathanLeffler: MSVC поддерживает это для C. На самом деле это PITA; Я пытаюсь скомпилировать заголовки Windows с помощью компилятора, отличного от MS C, но заголовки содержат повторяющиеся определения типов, которые нарушают компиляцию. - person arx; 19.07.2012
comment
@arx: Интересно — спасибо. Я запишу это; Я хотел бы добраться до точки, где я могу скомпилировать код, предназначенный для Windows, на машине Unix, используя заголовки на машине Unix для эмуляции среды компиляции Windows. Это было бы дополнительным усложнением, если бы я не мог использовать GCC 4.7.x с поддержкой C11 (которая у меня есть на соответствующих коробках). - person Jonathan Leffler; 19.07.2012