Повторные определения типов - недопустимо в C, но допустимо в C ++? - PullRequest
43 голосов
/ 21 декабря 2011

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

typedef struct Foo Foo;
typedef struct Foo Foo;

int main() { return 0; }

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

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

Ответы [ 5 ]

39 голосов
/ 21 декабря 2011

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

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

Ссылка:

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

§7.1.3.2:

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

[Пример:
typedef struct s {/ * ... * /} s;
typedef int I;
typedef int I;
typedef I I;
- конец примера]

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

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

Ссылка:

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

§6.2.2 / 6 состояния:

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

Далее §6.7 / 3 состояния:

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

21 голосов
/ 21 декабря 2011

Стандарт C теперь ISO / IEC 9989: 2011

Стандарт C 2011 года был опубликован ISO в понедельник 2011-12-19 (или, точнее, опубликовано уведомление о его публикации).веб-сайт комитета 19-го числа; стандарт, возможно, был опубликован так же давно, как 2011-12-08).Смотрите объявление на веб-сайте WG14 .К сожалению, PDF от ISO стоит 338 CHF, и от ANSI 387 USD .

  • Вы можете получить 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) стандартизировали это поведение.

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

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

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

Так что теперь есть явный мандат на повторное определение типа в C11.Получите доступность C11-совместимых C-компиляторов.


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

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

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

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


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

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

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

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

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

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

public.h

#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 */

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

private.h

#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 вы застряли с упражнением по переименованию для одной или обеих партий кода.

8 голосов
/ 21 декабря 2011

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

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

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

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

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

3 голосов
/ 16 марта 2014

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

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

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

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

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

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

3 голосов
/ 21 декабря 2011

В спецификации c нет ничего, что говорит , почему это неверно.Спецификация - это не то место, чтобы прояснить это.ПОЭТОМУ это разрешено в C1x (в соответствии с ответом, который я получил на один из моих последних вопросов).

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

...