重复的类型定义-在C中无效,但在c++中有效

Repeated typedefs - invalid in C but valid in C++?

本文关键字:无效 但在 有效 c++ 类型 定义      更新时间:2023-10-16

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

typedef struct Foo Foo;
typedef struct Foo Foo;
int main() { return 0; }

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

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

为什么在c++中编译?

因为c++标准明确这么说。

参考:

c++ 03标准7.1.3 typedef说明符

§7.1.3.2:

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

[示例:
typedef struct s{/*…]*/} s;
typedef int I;
typedef I I;
-end example]

为什么在C中编译失败?

typedef名称没有链接,C99标准不允许没有链接规范的标识符在相同的作用域和相同的名称空间中有多个声明。

参考:

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

§6.2.2/6州:

以下标识符没有链接:声明为非链接的标识符目标或功能;声明为函数形参的标识符;块作用域未声明存储类说明符的对象的标识符。

进一步§6.7/3州:

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

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

2011 C标准已于2011年12月19日星期一由ISO发布(或者更准确地说,发布的通知已于19日添加到委员会网站;该标准可能早在2011-12-08就已经发布了。请参阅WG14网站上的公告。遗憾的是,ISO的PDF文件价格为338瑞士法郎,, ANSI的PDF文件价格为387美元

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

主要解决途径问题是"C中允许重复类型定义吗?"答案是"不——不在ISO/IEC 9899:1999或9899:1990标准中"。原因可能是历史的;最初的C编译器不允许它,所以最初的标准化者(他们被授权标准化C编译器中已经可用的东西)标准化了这种行为。

请参阅al的回答,了解C99标准禁止重复类型的地方。C11标准已将§6.7¶3中的规则更改为:

3如果标识符没有关联,则该标识符的声明不得超过一次(在声明符或类型说明符中)具有相同的作用域和相同的名称空间,除了:

  • 一个类型定义名可以被重新定义,以表示与当前相同的类型。只要该类型不是可变修改类型;
  • 标签可以按照6.7.2.3的规定重新声明。

所以现在在C11中有一个明确的要求重复类型定义。启用兼容c11的C编译器


对于那些仍然使用C99或更早版本的人,接下来的问题可能是"那么我如何避免遇到重复类型定义的问题?">

如果你遵循一个头文件定义了多个源文件中需要的每个类型的规则(但是可以有多个头文件定义这些类型;但是,每个单独的类型只能在一个头文件中找到,如果在任何需要该类型的时候使用该头文件,那么就不会遇到冲突。

如果您只需要指向类型的指针,而不需要分配实际的结构或访问它们的成员(不透明类型),也可以使用不完整的结构声明。同样,要设置规则,确定哪个头文件声明了不完整类型,并在需要该类型的地方使用该头文件。

参见什么是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()私有头可能会这样做,或者需要polymath()服务的单个源文件可以在需要时包括它。

只要你虔诚地遵守纪律,包括正确的头,那么你就不会遇到双重定义的麻烦。

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

当然,可以有两个完全不可调和的头。对于一个人为的例子,如果有一个(设计糟糕的)头文件声明了FILE结构体的不同版本(与<stdio.h>中的版本不同),你就完蛋了;代码可以包括设计糟糕的头或<stdio.h>,但不能两者都包含。在这种情况下,设计糟糕的头应该修改为使用一个新名称(可能是File,但也可能是其他名称)。如果您必须在公司收购后将两个产品的代码合并为一个产品,并且使用一些常见的数据结构,例如用于数据库连接的DB_Connection,那么您可能会更实际地遇到这个问题。在没有c++namespace特性的情况下,您将不得不为一段或两段代码进行重命名练习。

你可以用c++来做,因为有7.1.3/3和/4。

不能在C99中这样做,因为它在6.7.7中没有任何等价的特殊情况,所以重新声明typedef名称遵循与重新声明任何其他标识符相同的规则。特别是6.2.2/6(类型定义没有链接)和6.7/3(没有链接的标识符只能在同一作用域内声明一次)。

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

[编辑:参见Johannes对C1x的回答。]我完全没有遵循这一点,所以我应该停止使用"C"来表示"C99",因为我可能甚至不会注意到他们何时批准和发布。这已经够糟糕的了:"C"应该是"C99"的意思,但在实践中意味着"如果你幸运的话是C99,但如果你必须支持MSVC,那就是C89"。

[再次编辑:确实,它已经出版了,现在是C11。哇。)

很多人都回答了,提到了标准,但没有人说,为什么C和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中。

c规范中没有说明为什么无效。说明书不是解释这一点的合适地方。在C1x中,这是允许的(根据我收到的对我最后一个问题的回答)。

我想这个c1x特性支持将宏转换为类型(如果相同,前者允许重复)。