为什么不始终包含所有标准标头

Why not include all the standard headers always?

本文关键字:标准 包含所 为什么不      更新时间:2023-10-16

我正在阅读赫伯·萨特(Herb Sutter(的More Exceptional C++和关于前向声明的第37项说:

切勿在转发声明就足够时#include标头。 宁愿只#include <iosfwd>当完整定义 不需要流。

我还听到了很多关于仅包含编译单元所需的标头以减少依赖性的建议。

我完全理解为什么这应该适用于项目标题,但我不太明白为什么包含不必要的标准标题是不好的。

例如,我做这样的事情:

//standard_library.h
#ifndef STANDARD_LIBRARY
#define STANDARD_LIBRARY
#include <iostream>
#include <chrono>
#include <thread>
...
// Everything I need in the project
#endif

并在任何地方都包含这个单一的标题,我需要从std那里得到一些东西

我能想象的问题是:

    不需要位于
  1. std 命名空间中的 C 库函数对命名空间的污染。
  2. 编译时间较慢

但是到目前为止,我对 1. 没有重大问题。几乎所有内容都在 std 命名空间中。我也不完全明白为什么 2.必然是一个重大问题。标准标头很少更改。另外据我所知,编译器可以预编译它们。当涉及到模板时,它们仅在我需要时才被实例化(编译(。

还有好处:

  1. 少打字
  2. 减少阅读
  3. 不太弄清楚我需要哪些标题以及某个函数在哪个标题中

是一个没有大项目经验的初学者程序员,我真诚地想弄清楚这一点,所以请怜悯我。

除了

  • 命名空间污染
  • 编译
  • 时间(虽然可以通过预编译的头文件来减少,但它会伤害那些编译大型项目的人一次,因为他们实际上想使用它,而不是开发 - 你也想考虑偶尔需要重建(

您命名为"较少弄清楚我需要哪些标头以及某个函数在哪个标头中"作为一种好处。到目前为止,我同意这对于设计良好的库和标头是正确的。

然而,在实践中,我遇到了一些错误(至少使用 MFC/ATL(一些错误,这些错误可以通过找出包含的正确顺序来解决。另一方面,有一天你想解决一个问题,这个问题让你在包含的头上旅行 - 现在想象一下你自己在看大量的头文件,实际上与你的代码文件无关。

我的结论是:如果您以后必须维护一个大型项目,那么通过包含一堆不必要的标头所节省的时间不会得到回报。您在开始包含任何标头之前投入的时间越多,之后您就越多时间 - 但大多数情况下没有真正识别它。

在您的系统上,它可能不会导致太大的减速,但其他人可能会有不同的体验。

从长远来看,计算机将继续变得更快,编译器将继续变得更有效率。在大多数小型项目中,痴迷于头文件所节省的时间肯定少于等待编译器所花费的增量时间。

但是(对于不预编译或缓存它们的实现(成本将在所有源文件中成倍增加。这会影响非增量构建的速度。

因此,对于用于多个源或分布在不同平台上的库,在公开发布之前,每隔一段时间就把东西删掉可能仍然是一个好主意。

哦!我知道一个好的。

我有一个专有库,用于从内存数据中制作漂亮的zip存档文件。它被设计为多平台,但显然没有在包括Windows在内的每个平台上都经过足够好的测试。

它在 Linux 和其他 POSIX 系统上运行良好,但当我试图在我的项目中采用它时,我一直在思考这个问题:如何在本地抑制 #define?

库和winbase.h(通过最标准的windows.h包含(都有一个CreateFile实体。而且,就像在winbase中一样,它只是一个宏,编译器看不到任何问题,除非您实际尝试在代码中使用CreateFile。

所以是的,保持命名空间干净可能是个好主意。

这个建议已经有10年的历史了。到现在为止有些过时了。计算机的速度提高了数百倍,存储从G变为T,疯狂的内存量闲置。

因此,过去的建议可能会退化。你走在询问原因的良好轨道上,如果你最终运行一些实验并形成自己的观点,可能会走上更好的轨道。

赫伯的项目比你的问题更笼统。C++(有点不幸的是(使用文件(翻译单元(模型进行编译 - 而不是源代码存储库/数据库。(那些尝试过IBM的Visual Age C++的人知道这是怎么回事;)。 这样做的结果是你把很多东西打包在一起。包含文件不是一个衬垫。

因此,当您需要包含一个标头以对某事进行单个声明时,您会发生拖入很多其他东西。

而那些只是为了编译的其他东西可能会拖入其他东西。以此类推递归。因此,如果您可以避免包含,它节省的不是一行而是数千行。并且包含可能不是一个文件,而是几十个文件。一种很好的经济方式。 当这些文件中的任何一个发生更改时,也需要重建,无论更改可能与您的内容无关。

假设您的标头使用 10 个不同类的指针。并且您包括定义它们的所有 10 个标头,而不仅仅是在用"class"作为使用前缀。这意味着任何可能只使用几个的客户端都会将所有十个作为依赖项拖入。 不经济。在我几年前的一个项目中使用了gtk++。.cpp文件只有几百行,但预处理器输出是 800k 或超过百万行。不开玩笑。虽然你付出了小冗余的代价:今天的东西可能是类的,但又是别的东西(比如模板的 typedef(。_fwd.h的想法减轻了这一点,但它实际上只是集中了冗余。 在实践中,我们在权衡中寻求一些平衡。

但所有这些想法并不适用于"稳定">无处不在的东西。在将 std:: 用于大量和自然使用的项目中,您可以看到每个源中包含的许多其他标头。因为它们被使用。如果今天一些重构删除了最后一个向量,它明天可能会重新生长。

在这里,选择实际上只是在包容发生的"地点"上,而经济则相反。设置一个供所有内容使用的"通用"标头可以消除其他文件中的大量噪音。 特别是如果系统支持这种情况。在VS中你有

  • 预编译标头,允许一次性编译公共材料并与其他 TU 共享结果
  • 强制包含,允许仅在项目中指定的公共标头,而不是在源文件中指定的公共标头
  • 属性表,包含在项目文件中,而不是手动使用这些设置

有了这样的支持,将许多甚至大多数标准标头放在该公共文件中可能是完全可行的,还有一些使用向量和其他通用名称的声明。

那些说你拖入许多可能引起冲突的名字的人是对的 - 但同时他们在实践中是错误的,因为最终有人会包括额外的标题,如果存在冲突,它将推翻船。 除非在项目中禁止使用 std:: 东西,否则我说出于不同目的重复使用其通用名称只是不好的做法。有人想用他自己的类字符串签入代码,并声称它肯定与 std::string 的前缀不同,我称之为"在我的尸体上"。 而对于罕见的名字来说,整理事故也没什么大不了的。

随着时间的推移

,什么是好的平衡,项目甚至项目内部都会发生变化。

原则上没有任何反对意见。

唯一会发生的事情是您的编译时间会增加,除非您当然创建该standard_library.h的预编译标头,在这种情况下,影响将很小。

请注意,大多数人更喜欢最小化其标头依赖项。这主要适用于您自己的头文件,在这种情况下,对源文件中未使用但包含的头文件进行少量更改可能会无缘无故地触发对该源文件的不必要重新编译,从而大大减慢增量构建的速度。

当标头更改时,程序会发生变化。 更改的程序需要进行测试。 选择性地最小化暴露,从而进行测试。

编译时间较慢

这是主要原因。

即使使用预编译标头,编译器也必须做更多的工作才能将标准库中的每个声明包含在项目的每个翻译单元中。

如果你有一个由数百个文件组成的大型项目,那么编译器将被调用数百次,每次调用都必须将整个标准库重新加载到内存中。

编译器

将使用更多内存来存储所有声明,并且在进行名称查找时必须检查更大的名称集(尽管编译器中体面的哈希表实现应该意味着这不会显着影响查找时间,只会影响内存使用情况。