【问题标题】:Good C header style良好的 C 标头样式
【发布时间】:2011-03-24 14:10:16
【问题描述】:

我的 C 标头通常类似于以下样式以避免多次包含:

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

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

#endif  /* !<FILENAME>_H */

然而,Rob Pike 在他的Notes on Programming in C 中对头文件提出了以下论点:

涉及#ifdef 的一些小动作可以防止文件被读取两次,但在实践中通常做错了 - #ifdef 存在于文件本身中,而不是包含它的文件中。结果通常是数千行不必要的代码通过词法分析器,这是(在好的编译器中)最昂贵的阶段。

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

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

【问题讨论】:

    标签: c coding-style header-files


    【解决方案1】:

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

    【讨论】:

    • 我觉得这个答案非常有用。有趣的是,我以一种更讽刺的方式表示同意:如果计算机可以完成我的工作,我为什么要这样做? ;)
    【解决方案2】:

    继续做你正在做的事情 - 它很清晰,不易出错,并且为编译器编写者所熟知,因此不像十年或两年前那样低效。

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

    【讨论】:

    • 如果 gcc 没有出错,它会在第一次包含标头时检测标准惯用语,记住文件名,并忽略所有将来包含该文件名的请求。这纯粹是编译器懒惰的情况。
    • @R.还是不行?我以为十年前每个主要编译器都会有这个。再说一次,读取一个文件两次并不像 89 年 Pike 写这些笔记时那样消耗资源。
    • @R..:它声称这样做 - gcc.gnu.org/onlinedocs/cppinternals/Guard-Macros.html。但是,它使用一些限制来确保优化有效,并且在限制被破坏的情况下可能会出现误报,但在这种情况下优化将是有效的。
    • 阅读两次是一种资源消耗。在现代机器上,仅仅一个系统调用就比解析一个小头文件需要更多的时间。
    【解决方案3】:

    Pike 在https://talks.golang.org/2012/splash.article 中写了更多关于它的文章:

    1984 年,Unix ps 命令的源代码ps.c 的汇编是 一直观察到#include &lt;sys/stat.h&gt; 37 次 预处理已经完成。即使内容被丢弃 36 这样做的时候,大多数 C 实现会打开文件,读取 它,并扫描它全部 37 次。事实上,如果没有很大的聪明,那 行为是由潜在复杂的宏语义所要求的 C 预处理器。

    编译器已经变得非常聪明:https://gcc.gnu.org/onlinedocs/cppinternals/Guard-Macros.html,所以现在这不是问题了。

    在 Google 上构建的单个 C++ 二进制文件可以打开和读取 数百个单独的头文件数万次。在 2007 年,Google 的构建工程师检测了 主要的谷歌二进制文件。该文件包含大约两千个文件, 如果简单地连接在一起,总共有 4.2 兆字节。到......的时候 #includes 已扩展,正在交付超过 8 GB 到编译器的输入,每个 C++ 增加 2000 个字节 源字节。

    作为另一个数据点,2003 年 Google 的构建系统从 单个 Makefile 到每个目录的设计,具有更好的管理,更多 显式依赖。一个典型的二进制文件大小缩小了大约 40%, 只是因为记录了更准确的依赖关系。即便如此, C++(或就此而言的 C)的属性使得验证是不切实际的 这些依赖是自动的,今天我们仍然没有 准确理解大谷歌的依赖需求 C++ 二进制文件。

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

    在计划 9 中,头文件被禁止包含更多 #include 子句;所有#includes 都必须在顶级C 文件中。当然,这需要一些纪律——程序员是 需要仅列出一次必要的依赖项,在 正确的顺序——但文档有所帮助,在实践中它非常有效 好吧。

    这是一个可能的解决方案。另一种可能性是拥有一个为您管理包含的工具,例如MakeDeps

    还有统一构建,有时称为 SCU,即单一编译单元构建。有一些工具可以帮助管理它,比如https://github.com/sakra/cotire

    使用针对增量编译速度进行优化的构建系统也很有优势。我说的是谷歌的 Bazel 和类似的。但是,它并不能保护您免受包含在大量其他文件中的头文件的更改。

    最后,有一个关于 C++ 模块的提案正在开发中,很棒的东西https://groups.google.com/a/isocpp.org/forum/#!forum/modules。另见What exactly are C++ modules?

    【讨论】:

    • 正如您在第二段引用的第一段中看到的那样,直到 2007 年,问题仍然很突出。如果只有更新的基准可用。
    • 根据web.archive.org/web/20021218032155/http://gcc.gnu.org/…,GCC 可以在 2002 年底进行多包含优化。我想知道 Google 在 2007 年使用的是什么编译器。
    【解决方案4】:

    您目前的做法是常用的方式。 Pike 的方法减少了一点编译时间,但是对于现代编译器来说可能不是很多(当 Pike 写他的笔记时,编译器不是优化器绑定的),它使模块变得混乱并且容易出错。

    您仍然可以通过不包含标头中的标头来减少多重包含,而是使用“在包含此标头之前包含 &lt;foodefs.h&gt;”来记录它们。

    【讨论】:

      【解决方案5】:

      我建议您将它们放在源文件本身中。无需抱怨在实际 PC 上需要解析数千行代码。

      此外 - 如果您检查每个包含该标头的源文件中的每个标头,则工作量和来源要多得多。

      而且您必须处理与默认头文件和其他第三方头文件不同的头文件。

      【讨论】:

        【解决方案6】:

        他可能在写这篇文章的时候发生了争执。如今,体面的编译器已经足够聪明,可以很好地处理这个问题。

        【讨论】:

        • @R.,嗯,参考,不,我没有。不过,不久前我在 SO 上读到过,包括警卫由 gcc 处理,相当于#pragma once。顺便说一句,#ifdef 的词法解析首先要查看需要真正解析的文件应该不会太难,不是吗?无论如何,我从来没有发现 this 是编译大型代码时的瓶颈。我经常使用-E 进行预编译,以查看预处理阶段产生了什么,这是肉眼无法测量的。
        【解决方案7】:

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

        我对 Rob Pike 为何建议他的方法的理论:他说的是 C,而不是 C++。

        在 C++ 中,如果您有很多类,并且您在自己的头文件中声明每个类,那么您将拥有很多头文件。 C 并没有真正提供这种细粒度的结构(我不记得见过很多单结构的 C 头文件),并且 .h/.c 文件对往往更大并且包含诸如模块或子系统之类的东西。因此,更少的头文件。在这种情况下,Rob Pike 的方法可能会奏效。但我认为它不适合非平凡的 C++ 程序。

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 2023-03-18
          • 2018-11-05
          • 1970-01-01
          • 1970-01-01
          • 2011-10-22
          • 2011-12-26
          • 1970-01-01
          相关资源
          最近更新 更多