重复的 typedef - 在 C 中无效但在 C++ 中有效?
Posted
技术标签:
【中文标题】重复的 typedef - 在 C 中无效但在 C++ 中有效?【英文标题】:Repeated typedefs - invalid in C but valid in C++? 【发布时间】:2012-01-25 13:37:53 【问题描述】:我想要一个标准参考,为什么以下代码会在 C 中触发合规性警告(使用 gcc -pedantic
进行测试;“typedef redefinition”),但在 C++ 中很好 (g++ -pedantic
):
typedef struct Foo Foo;
typedef struct Foo Foo;
int main() return 0;
为什么我不能在 C 中重复定义 typedef
?
(这对C project 的标题结构具有实际意义。)
【问题讨论】:
因为编译器对你失去了耐心。它第一次听到你。此外 - 敏捷/务实的编程适用于 DRY(不要重复自己)和 SPOT(单点真理)。如果你重复 typedef,你就违反了两者。 @JonathanLeffler:我的代码确实可以从 typedef 中受益,尽管在一致性方面。我目前的解决方法是不使用它并直接编写struct Foo
,但不知何故这与代码的风格不同。 (这与“实例化”这个hashtable "template"有关。)
也许你应该做please typedef struct Foo Foo;
,以安抚编译器。
FWIW,Clang 3.1 拒绝此代码并出现错误:“t.c:2:20: 错误:typedef 'Foo' 的重新定义在 C [-Wtypedef-redefinition] 中无效”。
FWIW:2011 C 标准于 2011 年 12 月 19 日星期一由 ISO 发布。请参阅WG14 网站上的公告。可悲的是,ISO 的 PDF 成本为 330 瑞士法郎。
【参考方案1】:
为什么用 C++ 编译?
因为 C++ 标准明确说明了这一点。
参考:
C++03 标准 7.1.3 typedef 说明符
§7.1.3.2:
在给定的非类作用域中,typedef 说明符可用于重新定义在该作用域中声明的任何类型的名称,以引用它已经引用的类型。
[示例: typedef struct s /* ... */ s; typedef int I; typedef int I; typedef 我我; ——结束示例]
为什么在 C 中编译失败?
typedef
名称没有链接,并且 C99 标准不允许没有链接规范的标识符具有多个具有相同范围和相同名称空间的声明。
参考:
C99 标准:§6.2.2 标识符的链接
§6.2.2/6 规定:
以下标识符没有链接:声明为除 一个对象或一个函数;声明为函数参数的标识符; 块作用域 未使用存储类说明符extern 声明的对象的标识符。
进一步§6.7/3 规定:
如果标识符没有链接,则标识符的声明(在声明符或类型说明符中)不得超过一个具有相同范围和相同名称空间的标识符,标签除外如 6.7.2.3 中所述。
【讨论】:
只是总结其他答案。 C 的下一个版本 C11 将允许这样做,从而消除 C++ 和 C 之间的不兼容之一。【参考方案2】:标准 C 现在是 ISO/IEC 9989:2011
2011 C 标准于 2011 年 12 月 19 日星期一由 ISO 发布(或者更准确地说,它已发布的通知已于 19 日添加到委员会网站;该标准可能已发布为'很久以前'作为2011-12-08)。请参阅WG14 网站上的公告。可悲的是,PDF from ISO 的价格为 338 瑞士法郎,而ANSI 的价格为 387 美元。
主要答案
问题是“C 中是否允许重复的 typedefs”?答案是“不——不在 ISO/IEC 9899:1999 或 9899:1990 标准中”。原因可能是历史原因;最初的 C 编译器不允许这样做,因此最初的标准化者(他们被授权对 C 编译器中已有的内容进行标准化)标准化了这种行为。
请参阅 Als 的 answer,了解 C99 标准禁止重复类型定义的位置。 C11 标准已将 §6.7 ¶3 中的规则更改为:
3 如果标识符没有链接,则标识符的声明不得超过一个 (在声明符或类型说明符中)具有相同的范围和相同的名称空间,除了 那:
可以重新定义 typedef 名称以表示与当前相同的类型, 前提是该类型不是可变修改的类型; 标签可以按照 6.7.2.3 中的规定重新声明。
所以现在在 C11 中有一个明确的要求重复 typedef。继续推出兼容 C11 的 C 编译器。
对于那些仍在使用 C99 或更早版本的人,接下来的问题大概是“那么我如何避免遇到重复 typedef 的问题?”
如果您遵循这样的规则,即有一个头文件定义了多个源文件中所需的每种类型(但可以有许多头文件定义此类类型;但每种单独的类型只能在一个头文件中找到) ,并且如果在需要该类型的任何时候都使用该标头,那么您就不会遇到冲突。
如果您只需要指向类型的指针并且不需要分配实际结构或访问它们的成员(不透明类型),您也可以使用不完整的结构声明。同样,设置关于哪个标头声明不完整类型的规则,并在需要该类型的任何地方使用该标头。
另见What are extern variables in 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 */
到目前为止,争议不大(尽管人们可以合理地怀疑这个库提供的接口非常不完整)。
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()
私有标头可能会这样做,或者需要@987654349 的单个源文件@services 可以在需要时包含它。
只要您认真遵守在任何地方都包含正确标题的纪律,那么您就不会遇到双重定义的麻烦。
如果您随后发现其中一个标头包含两组定义,一组可以在没有冲突的情况下使用,另一组有时(或总是)与某些新标头(以及其中声明的服务)冲突,那么您需要将原始标题拆分为两个子标题。每个子标题单独遵循此处详述的规则。原始标题变得微不足道 - 标题保护和包含两个单独文件的行。所有现有的工作代码都保持不变 - 尽管依赖项发生了变化(要依赖的额外文件)。新代码现在可以包含相关的可接受的子标头,同时还可以使用与原始标头冲突的新标头。
当然,您可以有两个根本不可调和的标题。举一个人为的例子,如果有一个(设计糟糕的)标头声明了 FILE
结构的不同版本(与 <stdio.h>
中的版本不同),那么您将被水洗;代码可以包含设计不良的标头或<stdio.h>
,但不能同时包含两者。在这种情况下,应该修改设计不佳的标头以使用新名称(可能是File
,但也可能是其他名称)。如果您必须在公司收购后将两个产品的代码合并为一个,并使用一些常见的数据结构,例如用于数据库连接的DB_Connection
,您可能会更实际地遇到这个麻烦。在没有 C++ namespace
功能的情况下,您会被困在对一组或多组代码进行重命名练习。
【讨论】:
是的,我非常需要“不完整的结构声明”,因为单独的预处理器复杂性会禁止某些包含。所以你的意思是如果它们被完整的标题再次类型定义,我必须不 typedef那些前向声明? 这基本上是没有答案的。 @Jens:为什么?对直接问题有一个直接(但简短)的答案,以及一个更长的更具论述性的答案,它解释了如何解决那些往往会导致想要首先提出直接问题的问题。还有关于 C11 标准的旁注(我想这可能被认为是题外话)。 @JonathanLeffler,您的答案的开头与问题完全无关。从旁注开始并不是让某人进一步阅读的好主意。据我记得,在我发表评论后,您对“否”(不是 C99)的精确度已添加(并大大提高了可读性)。【参考方案3】:由于 7.1.3/3 和 /4,您可以在 C++ 中执行此操作。
您不能在 C99 中这样做,因为它在 6.7.7 中没有任何等效的特殊情况,因此重新声明 typedef 名称遵循与重新声明任何其他标识符相同的规则。特别是 6.2.2/6(typedef 没有链接)和 6.7/3(没有链接的标识符只能在相同范围内声明一次)。
记住typedef
在 C99 中是一个存储类说明符,而在 C++ 中它是一个声明说明符。不同的语法让我怀疑 C++ 作者决定投入更多精力使 typedef 成为“一种不同类型的声明”,因此很可能愿意为他们花费更多时间和文本来制定特殊规则。除此之外,我不知道 C99 作者的(缺乏)动机是什么。
[编辑:见约翰内斯对 C1x 的回答。我根本没有遵循这一点,所以我可能应该停止使用“C”来表示“C99”,因为我什至可能不会注意到他们何时批准和发布。已经够糟糕了:“C”应该表示“C99”,但实际上表示“如果你幸运的话是C99,但如果你必须支持MSVC,那就是C89”。]
[再次编辑:确实,它已经发布,现在是 C11。呜呜呜。]
【讨论】:
您能详细说明“存储类”与“decl-”说明符吗? @Kerrek:看一下相关标准中的语法:C++ decl-specifier-seq 的等价物就是C declaration-specifiers .请注意,除了 C 中列出的内容之外,C++ 还允许使用关键字friend
和constexpr
,它们也是特殊类型的声明。它将typedef
移出storage-class-specifier 并移入decl-specifier。差异并不能证明什么,它只是定义语法的不同方式,但它向我表明 C++ 认为“让我们有几种不同的声明”,而 C 认为“让我们用最少的麻烦来适应 typedef”。【参考方案4】:
很多人都回答过,指的是标准,但没有人说,为什么这里的 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++,则 typedef 会重复,因为 C++ 语言添加了自己的隐式 typedef。因此,为了避免程序员删除那些不再需要的 typedef 的麻烦,他们从一开始就允许在 C++ 中使用重复的 typedef。
正如其他人所说,有时间的人意识到,在 C 中允许重复相同的 typedef 也是有用的。至少,它不应该伤害。这就是为什么这个 C++ 特性被“向后移植”到 C11 中的原因。
【讨论】:
【参考方案5】:c 规范中没有说明 为什么 这是无效的。规范是澄清这一点的错误地方。 FWIW 在 C1x 中是允许的(根据我收到的最后一个问题的答案)。
我想这个 c1x 功能支持将宏转换为 typedef(如果相同,则允许重复前者)。
【讨论】:
有趣!现在,MS 需要多长时间才能提供符合 C1x 的编译器以便我们使用它? 所以澄清一下,在 C 中这是被禁止的,原因与int x; int x;
?
@Jonathan Leffler:一段无限长的字符串有多长?
恐怕比我准备等待的时间还要长。 :(
@JonathanLeffler:不要害怕,世界上到处都是编译器供应商,他们渴望达到 C1x 合规性。以上是关于重复的 typedef - 在 C 中无效但在 C++ 中有效?的主要内容,如果未能解决你的问题,请参考以下文章
避免重复代码:typedef/使用具有默认参数的模板类 (C++14)
为啥从字符串常量转换为 'char*' 在 C 中有效但在 C++ 中无效
double ** a = malloc() 在 c 中有效,但在 cuda 中无效?