在C语言中重复的typedef是无效的,但在C++中是有效的吗?

52

我想要一个标准参考,解释为什么以下代码在C语言中会触发一条符合性警告(使用gcc -pedantic测试; "typedef重定义"),但在C++中则是可以的(使用g++ -pedantic):

typedef struct Foo Foo;
typedef struct Foo Foo;

int main() { return 0; }

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

(这对于一个C项目的头文件结构有实际的影响.)


7
因为编译器对你失去了耐心。它第一次就听到了你的请求。此外,敏捷/实用编程追求DRY(不要重复自己)和SPOT(单一真相)。如果你重复typedef,那么你就会违反这两个原则。 - Jonathan Leffler
@JonathanLeffler:我拥有的代码可以从typedef中受益良多,尤其是在统一性方面。我目前的解决方法是不使用它并直接编写struct Foo,但这与代码风格不符。 (这与“实例化”此hashtable“模板”有关)。 - Kerrek SB
15
也许你应该这样写 请使用 typedef struct Foo Foo;,以平息编译器。 - R. Martinho Fernandes
1
就此而言,Clang 3.1会拒绝这段代码并报错:"t.c:2:20: error: redefinition of typedef 'Foo' is invalid in C [-Wtypedef-redefinition]"。 - Xeo
3
值得一提的是,2011年版的C语言标准已经由国际标准化组织(ISO)于2011年12月19日星期一发布。请在 WG14 网站查看公告。不幸的是,ISO发布的PDF文件售价为330瑞士法郎。 - Jonathan Leffler
显示剩余3条评论
5个回答

45

为什么这段代码可以在C++中编译通过?

因为C++标准明确规定了这样做是可以的。

参考文献:

C++03标准 7.1.3 typedef specifier

§7.1.3.2:

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

[例如:
typedef struct s { /* ... */ } s;
typedef int I;
typedef int I;
typedef I I;
—end example]

为什么这段代码无法在C语言中编译通过?

typedef名称没有链接性,而C99标准禁止具有相同作用域和名称空间内多个声明的标识符没有链接说明符。

参考文献:

C99标准:§6.2.2 链接指示符

§6.2.2/6说明:

以下标识符没有链接性:除对象或函数之外的任何内容声明的标识符;声明为函数参数的标识符;在没有extern存储类说明符的情况下声明的对象的块作用域标识符。

进一步的,§6.7/3说明:

如果标识符没有链接性,在具有相同作用域和名称空间内仅能有一个声明该标识符(在声明符或类型说明符中),除非在6.7.2.3中指定了标记。


15
简单总结其他回答。下一个版本的C语言,即C11,将允许这样做,并因此消除C++和C之间的不兼容性之一。 - Jens Gustedt

26

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

2011年的C标准于2011年12月19日星期一由ISO(更确切地说,是它被发布的通知被添加到委员会网站上,并且该标准可能已经在2011年12月08日左右被发布)发布。请参见WG14网站上的公告。不幸的是,来自ISO的PDF的费用为338瑞士法郎,而来自ANSI的费用为387美元

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

主要答案

问题是“C语言中是否允许重复的typedef声明”?答案是“不允许-ISO/IEC 9899:1999或9899:1990标准都不允许”。原因可能是历史原因;最初的C编译器不允许它,因此最初的标准化者(他们被授权标准化已在C编译器中可用的内容)规范了这种行为。
请参见Alsanswer,了解C99标准禁止重复typedef的位置。C11标准在§6.7 ¶3中改变了规则,如下所示:
“如果标识符没有链接,则在相同作用域和相同名称空间中,不能有超过一个声明该标识符(在声明符或类型说明符中),但可以重新定义typedef名称以表示与当前相同的类型,前提是该类型不是可变修改的类型;可以按照6.7.2.3中指定的方式重新声明标签。”
因此,现在C11中明确要求重复typedef。期待C11兼容的C编译器的推出。
对于仍在使用C99或更早版本的人,随后的问题可能是“那么我如何避免重复typedef的问题?” 如果您遵循这样的规则:有一个单独的头文件定义了每个在多个源文件中需要的类型(但可以有许多定义此类类型的头文件;每个单独的类型仅在一个头文件中找到),并且在需要该类型时使用该头文件,则不会出现冲突。 如果您只需要指向类型的指针而不需要分配实际结构或访问它们的成员(不完整的类型声明),则还可以使用不完整的结构声明。同样,设置有关哪个头文件声明不完整类型的规则,并在需要该类型的任何地方使用该头文件。请参见C中的extern变量是什么;它谈论变量,但类型可以被类似地处理。

来自评论的问题

由于独立预处理器的复杂性,我非常需要“不完整结构声明”。那么你是说如果这些前向声明被完整头文件再次typedef,则我不能typedef它们吗?

多少是这样。我没有真正遇到过这个问题(尽管工作中有些系统非常接近必须担心这个问题),所以这有点暂定,但我相信它应该有效。

通常情况下,头文件以足够的细节描述了“库”(一个或多个源文件)提供的外部服务,以便库的用户能够编译它。特别是在有多个源文件的情况下,可能还会有一个内部头文件,例如定义完整类型的头文件。

所有头文件都是(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 */

到目前为止,还没有太多争议(尽管可以合理地怀疑该库提供的接口非常不完整)。

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()服务的单个源文件在需要时可以包括它。

只要您严格遵循正确地包含头文件的纪律,就不会遇到重复定义的问题。

如果随后发现您的某个标头包含两组定义,其中一组可以无冲突使用,而另一组可能(或总是)与某个新标头(及其中声明的服务)发生冲突,则需要将原始标头拆分为两个子标头。每个子标头都遵循此处详述的规则。原始标头变得微不足道-一个头文件保护和包括两个单独文件的行。所有现有的工作代码保持不变-尽管依赖关系发生了变化(额外的文件要依赖)。现在,新代码可以包括相关的可接受子标头,同时使用与原始标头冲突的新标头。

当然,你可以拥有两个头文件,它们之间是无法调和的。举个人为的例子,如果有一个(设计不良的)头文件声明了一个与<stdio.h>中版本不同的FILE结构,那么你就没办法了;代码可以包含设计不良的头文件或<stdio.h>,但不能同时包含。在这种情况下,应该修改设计不良的头文件,使用一个新名称(也许是File,但也可能是其他名称)。如果你需要将两个产品的代码合并成一个,在公司收购后遇到一些共同的数据结构,例如数据库连接的DB_Connection,那么你更有可能遇到这种麻烦。在没有C++namespace功能的情况下,你只能对其中一个或两个代码进行重命名操作。

是的,由于独立的预处理器复杂性禁止某些包含,我非常需要“不完整的结构声明”。所以你的意思是说,如果这些前向声明在完整头文件中再次进行了typedef,则我不能对它们进行typedef吗? - Kerrek SB
这基本上是一个无关紧要的回答。 - Jens Gustedt
2
@Jens:为什么?对于直接的问题,有一个直接(但简短)的答案,还有一个更长的、更详细的回答,解释了如何解决导致首先想要问直接问题的问题。还有一些关于 C11 标准的附注(可能被视为离题)。 - Jonathan Leffler
@JonathanLeffler,你的回答开头完全没有与问题相关的内容。从侧面开始不是一个让人继续阅读的好主意。就我所记得的而言,你对于“no”(不适用于C99)的精度是在我发表评论之后添加的(并且大大提高了可读性)。 - Jens Gustedt

9
由于 7.1.3/3 和 /4,您可以在 C++ 中做到这一点。
在 C99 中,您无法这样做,因为它在 6.7.7 中没有任何等效的特殊情况,因此重新声明 typedef 名称遵循与重新声明任何其他标识符相同的规则。具体来说是 6.2.2/6(typedef 没有链接)和 6.7/3(没有链接的标识符只能在相同作用域内声明一次)。
请记住,在 C99 中,typedef 是一种存储类说明符,而在 C++ 中,它是一种声明说明符。不同的语法使我怀疑 C++ 作者决定更加努力地使 typedef 成为“不同类型的声明”,因此可能愿意花更多的时间和文字来制定特殊规则。除此之外,我不知道 C99 作者缺乏动机是什么。
[编辑:有关 C1x,请参见 Johannes 的答案。我根本没有关注,所以我应该停止使用“C”表示“C99”,因为当他们批准和发布时,我可能甚至不会注意到。现在情况已经够糟糕了:“C”应该表示“C99”,但实际上表示“C99 如果你很幸运,如果你必须支持 MSVC,则表示 C89”。]
[再次编辑:实际上,它已经被发布,并且现在是 C11。赞美。]

3
您能详细说明“存储类”与“声明说明符”的区别吗? - Kerrek SB
@Kerrek:看看相关标准中的语法:C++ 中的 decl-specifier-seq 等效于 C 中的 declaration-specifiers。需要注意的是,除了 C 中列出的内容之外,C++ 还允许关键字 friendconstexpr,它们也是特殊类型的声明。C++ 将 typedefstorage-class-specifier 移动到 decl-specifier 中。这种差异并不能证明什么,只是一种不同的定义语法方式,但它表明 C++ 认为“让我们拥有几种不同类型的声明”,而 C 则认为“让我们尽可能简单地容纳 typedef”。 - Steve Jessop

6
很多人都已经回答了标准的问题,但没有人说为什么 C 和 C++ 的标准不同。我相信,C++ 允许重复使用 typedef 的原因是,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 ++,则typedefs会成为重复的,因为C ++语言添加了自己的隐式typedefs。因此,为避免程序员删除那些不再需要的typedefs的麻烦,从一开始就允许在C ++中使用重复的typedefs。
正如其他人所说,有时间的人们意识到,在C中允许重复的相同typedefs也可能很有用。至少,它不会损害。这就是为什么这个C ++功能被“倒退”到C11的原因。

3

在C语言规范中没有说明为什么这是无效的。规范不是澄清这一点的正确位置。顺便说一下,在C1x中允许这样做(根据我上一个问题的答案)。

我认为,这个C1x特性支持将宏转换为typedefs(如果宏完全相同,则前者允许重复)。


2
有趣!现在,微软什么时候提供符合C1x标准的编译器,以便我们可以使用它呢? - Jonathan Leffler
所以澄清一下,在C语言中,这是被禁止的,原因与“int x; int x;”相同? - Kerrek SB
@Jonathan Leffler:无限长的字符串长度是多少呢? - Steve Jessop
@JonathanLeffler:不用担心,世界上有很多编译器供应商都渴望达到C1x的兼容性。 - Stephen Canon
@KerrekSB: int x; int x; 在 C 语言中是合法的(在文件作用域),它只是两个具有相当接近的 外部链接对象 的试探性定义。 - CB Bailey
显示剩余5条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接