2011-03-24 85 views
9

我的C头文件通常与以下类似的风格,避免多次包含:在他Notes on Programming in C良好的C头风格

#ifndef <FILENAME>_H 
#define <FILENAME>_H 

// define public data structures/prototypes, macros etc. 

#endif /* !<FILENAME>_H */ 

然而,罗勃·派克提出了以下论据有关的头文件:

有一个涉及#ifdef的小舞蹈可以防止文件被读两次,但在实践中通常是错误的 - #ifdef位于文件本身中,而不是包含它的文件。结果往往是成千上万的不必要的代码行通过词法分析器,这是(在良好的编译器中)最昂贵的阶段。

一方面,派克是我真正佩服的唯一程序员。另一方面,将多个#ifdef放在多个源文件中而不是将一个#ifdef放在单个头文件中感觉不必要地尴尬。

处理多重包含问题的最佳方法是什么?

回答

11

在我看来,使用需要较少时间的方法(这可能意味着将#ifdefs放在头文件中)。如果编译结果更清晰,我不介意编译器是否必须更加努力。如果你可能正在研究一个数百万行的代码库,你必须不断完全重建,也许额外的节省是值得的。但在大多数情况下,我怀疑额外的成本通常并不明显。

+2

我觉得这个答案非常有用。有趣的是,我同意一个更讽刺的方式:如果电脑可以做我的工作,为什么我会这样做? ;) – 2011-03-24 14:20:01

6

继续做你所做的事 - 很明显,错误发生率较低,并且由编译器编写者所熟知,所以不像十年或两年前那样效率低下。

您可以使用非标准的#pragma once - 如果您搜索,一旦讨论,可能至少有一个书架的价值包括警卫vs杂记,所以我不会推荐一个。

+0

见,如果GCC不吸,它将检测标准成语首次被列入了头,还记得文件名,而忽略所有将来的请求包含这个文件名称。这完全是编译器懒惰的一个例子。 – 2011-03-24 14:20:17

+1

@R。它仍然没有?我曾认为每个主要的编译器都会在十年前拥有这个版本。然后,再次,两次读取文件并不是真正的当派克写这些笔记时它是在89年的资源。 – Erik 2011-03-24 14:22:12

+1

@R:它声称这么做 - http://gcc.gnu.org/onlinedocs/cppinternals/Guard-Macros.html。但是,它使用一些限制来确保优化是有效的,并且在限制被破坏的情况下可能存在误报,但是在这种情况下优化将是有效的。 – 2011-03-24 15:06:34

2

你现在这样做的方式是常用的方法。派克的方法在编译时间上略微减少了一些,但现代编译器可能并不是很多(当派克写他的笔记时,编译器并不是优化器绑定的),它使模块和它的错误发生混乱。

您仍然可以通过不包含来自标题的标题来削减多重包含,而是将它们记录为“在包含此标题之前包含<foodefs.h>”。

1

我建议你把它们放在源文件本身。没有必要抱怨几千个不必要的解析代码与实际PC的代码行。

此外 - 如果您检查每个包含标题的源文件中的每个标题,它的工作量和来源将会更多。

而且您将不得不处理与默认和其他第三方标题不同的头文件。

1

他写这篇文章的时候可能会有争论。时下正派的编译器足够聪明地处理这个问题。

+0

参考?我发现他们不够聪明...... – 2011-03-24 14:22:49

+1

@R。,嗯,参考,不,我没有。尽管如此,我在不久之前就读过这样的文章,其中包括警卫被gcc等同于'#pragma once'。顺便说一下'#ifdef'的词法分析首先看到文件的真实内容应该不会太难,不是吗?无论如何,我从来没有在编译大代码时发现* this *是一个瓶颈。我经常用'-E'进行预编译,看看预处理阶段产生了什么,这是人眼无法衡量的。 – 2011-03-24 14:40:09

0

我同意你的方法 - 正如其他人所评论的那样,它更清晰,自我记录和更低的维护。

我的理论是为什么Rob Pike可能提出了他的方法:他在谈论C而不是C++。

在C++中,如果您有很多类,并且您在每个头文件中声明了每个类,那么您将拥有很多头文件。 C并没有真正提供这种细粒度的结构(我不记得看到许多单结构C头文件),并且.h/.c文件对倾向于更大并且包含像模块或一个子系统。所以,更少的头文件。在那种情况下,Rob Pike的方法可能会起作用。但我不认为它适合于非平凡的C++程序。

3

派克在https://talks.golang.org/2012/splash.article写了一些更多一点:

在1984年,ps.c汇编,源到Unix ps命令,是 由时间所有 预处理过观察到#include <sys/stat.h> 37倍已完成。尽管这样做会丢弃内容,但大多数C实现会打开该文件,读取它的内容 ,并将其全部扫描37次。事实上,如果没有太大的巧妙,那么 C预处理器的潜在复杂宏语义就需要该行为。

因为:https://gcc.gnu.org/onlinedocs/cppinternals/Guard-Macros.html,编译器变得相当聪明,所以这个问题现在已经不大了。

在Google上构建一个单独的C++二进制文件可以打开并阅读 数百个单独的头文件数万次。在2007年,Google的建造工程师负责编译 主要Google二进制文件。该文件包含大约两千个文件,如果简单连接在一起,则总计为4.2兆字节。到 时,#includes已被扩展,超过8千兆字节正在被传送到编译器的输入 ,对于每个C++ 源字节都会产生2000字节的爆炸。作为另一个数据点,2003年,Google的构建系统从单一Makefile的 移动到了具有更好管理,更多 显式依赖关系的按目录设计。一个典型的二进制文件会缩小约40%的文件大小, 只是由于记录了更精确的依赖关系。即便如此,C++(或C的属性)的自动验证 这些依赖关系是不切实际的,而今天我们仍然没有准确理解大型Google OLE二进制文件的依赖性需求。

有关二进制大小的观点仍然相关。编译器(链接器)在剥离未使用的符号方面非常保守。 How to remove unused C/C++ symbols with GCC and ld?

在Plan 9,头文件从含有进一步 #include条款禁止;所有#includes都需要位于顶级C文件中。这需要一些纪律,当然,程序员的是,列出必要的依赖恰好一次需要 ,该 正确的顺序,但是文档中的帮助,并在实践中效果很 好。

这是一个可能的解决方案。另一种可能性是有一个工具来管理你的包装,例如MakeDeps

还有统一建立,有时也被称为SCU,单一的编译单元建立。有工具来帮助管理,像https://github.com/sakra/cotire

使用渐进式编译的速度优化可能是有利的过于构建系统。我正在谈论Google的Bazel和类似的。尽管如此,它并不能保护包含在大量其他文件中的头文件的更改。

最后,对C++的作品模块的提案,伟大的东西https://groups.google.com/a/isocpp.org/forum/#!forum/modules。也What exactly are C++ modules?

+0

从第二段引文的第一段可以看出,迟到2007年,问题依然突出。如果只有更新的基准可用。 – FRIdSUN 2015-11-26 13:29:43

+0

据https://web.archive.org/web/20021218032155/http://gcc.gnu.org/onlinedocs/cppinternals/Guard-Macros.html,GCC可以做多,包括优化已经在2002年年底。我想知道Google在2007年使用的编译器是什么。 – user7610 2015-11-26 17:41:51