重复的typedef - 在C中无效但在C++中有效吗?

Ker*_* SB 44 c c++ typedef language-lawyer

我想要一个标准的参考,为什么下面的代码在C中触发了一个合规性警告(用gcc -pedantic"typedef redefinition" 测试过),但是在C++(g++ -pedantic)中很好:

typedef struct Foo Foo;
typedef struct Foo Foo;

int main() { return 0; }
Run Code Online (Sandbox Code Playgroud)

为什么我不能typedef在C中重复定义?

(这对C项目的头结构有实际意义.)

Alo*_*ave 39

为什么用C++编译?

因为C++标准明确地这样说.

参考:

C++ 03标准7.1.3 typedef说明符

§7.1.3.2:

在给定的非类作用域中,可以使用typedef说明符重新定义该作用域中声明的任何类型的名称,以引用它已引用的类型.

[例如:
typedef struct s {/*...*/} s;
typedef int I;
typedef int I;
typedef II;
- 末端的例子]

为什么这不能用C编译?

typedef 名称没有链接,C99标准不允许没有链接规范的标识符具有多个具有相同范围和相同名称空间的声明.

参考:

C99标准:§6.2.2标识符的链接

§6.2.2/ 6规定:

以下标识符没有链接:标识符被声明为除了对象或函数之外的任何其他标识符; 声明为函数参数的标识符; 一个块范围标识符,用于在没有存储类特定的情况下声明的对象.

进一步§6.7/ 3规定:

如果标识符没有链接,则除了6.7.2.3中指定的标记之外,标识符(在声明符或类型说明符中)的声明不得超过一个具有相同作用域和相同名称空间的声明.

  • 只是总结其他答案.下一个版本的C,C11将允许这样做,从而消除C++和C之间的不兼容性之一. (10认同)

Jon*_*ler 21

标准C现在是ISO/IEC 9989:2011

2011年C标准于2011年12月19日由ISO发布(或者更准确地说,它已经发布的通知在19日被添加到委员会网站;该标准可能已经发布为'很久以前'作为2011-12-08).请参阅WG14网站上的公告.遗憾的是,ISOPDF成本为338瑞士法郎,ANSI为 387美元.

  • 您可以从ANSI获得INCITS/ISO/IEC 9899:2012(C2011)的PDF格式,价格为30美元.
  • 您可以从ANSI获得INCITS/ISO/IEC 14882:2012(C++ 2011)的PDF格式,价格为30美元.

主要答案

问题是"C中是否允许重复的typedef"?答案是"不 - 不符合ISO/IEC 9899:1999或9899:1990标准".原因可能是历史性的; 最初的C编译器不允许它,所以原始的标准化程序(被授权标准化C编译器中已有的东西)标准化了这种行为.

请参阅Als答案,了解C99标准禁止重复的typedef的位置.C11标准已将§6.73中的规则更改为:

3如果标识符没有链接,则标识符的声明(在声明符或类型说明符中)不得超过一个具有相同作用域和相同名称空间的声明,但以下情况除外:

  • 如果类型不是可变修改类型,则可以重新定义typedef名称以表示与其当前相同的类型;
  • 标签可以按照6.7.2.3中的规定重新声明.

所以现在C11中有一个重复的typedef明确的命令.推广符合C11标准的C编译器.


对于仍然使用C99或更早版本的人来说,后续问题可能是"那么如何避免遇到重复typedef的问题?"

如果您遵循以下规则:单个标头定义了多个源文件中所需的每种类型(但可以有许多标头定义此类型;但每个单独的类型只能在一个标头中找到),如果在需要该类型的任何时候使用该标头,那么您不会遇到冲突.

如果只需要指向类型的指针,并且不需要分配实际结构或访问它们的成员(opaque类型),也可以使用不完整的结构声明.再次,设置关于哪个标头声明不完整类型的规则,并在需要类型的地方使用该标头.

另请参见C中的外部变量是什么 ; 它讨论变量,但类型可以稍微类似地对待.


来自评论的问题

由于单独的预处理器并发症禁止某些夹杂物,我非常需要"不完整的结构声明".所以你说我必须不输入那些前向声明,如果它们被完整的标题再次输入了吗?

或多或少.我真的不必处理这个问题(虽然有些系统在工作中非常接近而不必担心),所以这有点暂时,但我相信它应该有效.

通常,标题描述了由"库"(一个或多个源文件)提供的外部服务,其足够详细,以便库的用户能够使用它进行编译.特别是在存在多个源文件的情况下,还可以存在内部头部,其定义例如完整类型.

所有标题都是(a)自包含和(b)幂等.这意味着您可以(a)包含标头,并自动包含所有必需的其他标头,并且(b)您可以多次包含标头而不会引起编译器的愤怒.后者通常用头部防护装置实现,虽然有些人更喜欢#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 */
Run Code Online (Sandbox Code Playgroud)

到目前为止,所以不是很有争议(尽管人们可以合理地怀疑这个库提供的界面非常不完整).

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 */
Run Code Online (Sandbox Code Playgroud)

再次,不是很有争议.首先public.h必须列出标题; 这提供了自我控制的自动检查.

消费者代码

任何需要polymath()服务的代码都会写:

#include "public.h"
Run Code Online (Sandbox Code Playgroud)

这就是使用该服务所需的所有信息.

供应商代码

库中定义polymath()服务的任何代码都写入:

#include "private.h"
Run Code Online (Sandbox Code Playgroud)

此后,一切正常.

其他提供商代码

如果有另一个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功能,您将无法使用一个或两个代码进行重命名练习.

  • @Jens:为什么?对直接问题有一个直接(但简短)的答案,以及一个更长的话语答案,它解释了如何解决往往导致想要直接提出问题的问题.关于C11标准的附注(我想这可能被认为是偏离主题的). (2认同)

Ste*_*sop 8

由于7.1.3/3和/ 4,您可以在C++中完成.

您无法在C99中执行此操作,因为它在6.7.7中没有任何等效的特殊情况,因此重新声明typedef名称遵循与重新声明任何其他标识符相同的规则.特别是6.2.2/6(typedef没有链接)和6.7/3(没有链接的标识符只能用相同的范围声明一次).

记住typedef是C99中的存储类说明符,而在C++中它是一个decl-specifier.不同的语法让我怀疑C++作者决定投入更多精力来使typedef成为"另一种声明",因此很可能愿意花更多的时间和文字来制定特殊的规则.除此之外,我不知道C99作者的动机是什么(缺乏).

[编辑:见约翰内斯对C1x的回答.我根本不遵循这一点,所以我应该停止使用"C"来表示"C99",因为我可能甚至不会注意到他们批准和发布时.这很糟糕,因为它是:"C"应该是"C99",但在实践中意味着"C99,如果你很幸运,但如果你必须支持MSVC,那么C89".

[再次编辑:事实上,它已经出版,现在是C11.活泉]

  • 你能详细说明"存储类"与"decl - "说明符吗? (3认同)

Kai*_*zke 5

很多人已经回答了,参考标准,但没有人说,为什么这里C和C++的标准不同。好吧,我相信,在 C++ 中允许重复 typedef 的原因是,C++ 隐式地将结构和类声明为类型。所以以下在 C++ 中是合法的:

struct foo { int a; int b; };
foo f;
Run Code Online (Sandbox Code Playgroud)

在 C 中,必须这样写:

struct foo { int a; int b; };
typedef struct foo foo;
foo f;
Run Code Online (Sandbox Code Playgroud)

有很多这样的 C 代码,将结构声明为类型。如果将此类代码迁移到 C++,则 typedef 会重复,因为 C++ 语言添加了自己的隐式 typedef。因此,为了避免程序员删除那些不再需要的 typedef 的麻烦,他们从一开始就允许在 C++ 中使用重复的 typedef。

正如其他人所说,随着时间的推移人们意识到,在 C 中允许重复相同的 typedef 也是有用的。至少,它不应该受到伤害。这就是为什么这个 C++ 特性被“反向移植”到 C11 中的原因。