大型项目中的C++头文件策略(再探讨)

10
我已经阅读了所有关于这个主题的内容,包括在这个网站上的几次非常有帮助的讨论、NASA编码指南和Google C++指南。我甚至买了这里推荐的“物理C++设计”书籍(抱歉,忘记名字了),并从中得到了一些有用的想法。大多数来源似乎都同意——头文件应该是自包含的,也就是说,它们包含所需的内容,以便cpp文件可以包含头文件而不包含任何其他文件,并且可以编译。我也明白尽可能使用前向声明而不是包含的重要性。
话虽如此,如果foo.cpp包含bar.h和qux.h,但事实证明bar.h本身包含qux.h呢?那么foo.cpp是否应避免包含qux.h?优点:清理了foo.cpp(减少了"噪音")。缺点:如果有人更改bar.h以不再包含qux.h,则foo.cpp会神秘地开始无法编译。还会导致foo.cpp和qux.h之间的依赖关系不明显。
如果你的答案是“cpp文件应该#include它需要的每个头文件”,那么从逻辑上推论,这意味着几乎每个cpp文件都必须包括、等,因为大多数代码最终都会使用它们,如果你不能依赖其他头文件包含它们,那么你的cpp文件需要显式地包含它们。这在cpp文件中似乎是很多的"噪音"。
你有什么想法?

之前的讨论:

有哪些限制C++项目编译依赖的技巧?

你在大型项目中更喜欢的C/C++头文件策略是什么?

如何自动查找未使用的#include指令?

预计完成时间:受此前在这里的讨论启发,我编写了一个Perl脚本来逐个注释掉每个“include”和“using”,然后尝试重新编译源文件,以找出不需要的内容。 我还弄清楚了如何将其与VS 2005集成,因此您可以双击转到“未使用”的包含文件。 如果有人需要它,请告诉我...现在还处于实验阶段。


我的猜测是,由于这可能是主观的,所以这应该是社区维基。 - GManNickG
这本书可能是John Lakos的《大规模C++软件设计》:http://www.amazon.com/Large-Scale-Software-Addison-Wesley-Professional-Computing/dp/0201633620 - Greg Hewgill
@Michael:如果是Lakos(我也怀疑),那么这是一篇非常好的文章,但由于它非常古老,所以要持保留态度。(甚至可能需要一锅盐。我读过它已经很久了。) - sbi
是的,就是这个;对于C++物理设计,它基本上是唯一的选择。我发现它非常有启发性,而且并不过时。这些原则仍然适用于C++开发。 - user152154
+1 给 Lakos;优秀的书。 - Len Holgate
显示剩余3条评论
7个回答

8
如果你的答案是“一个cpp文件应该包含它需要的每个头文件”,那么从逻辑上讲,这意味着几乎每个cpp文件都必须包含、等,因为大多数代码最终都会使用它们,如果你不应该依赖于其他头文件包含它们,那么你的cpp需要显式地包含它们。
是的。这是我喜欢的方式。
如果“噪音”太大难以忍受,可以有一个“全局”包含文件,其中包含常见的一组包括(就像很多Windows程序中的stdafx.h一样),并在每个.cpp文件的开头包含它(这也有助于预编译头文件)。

实际上,stdafx 是我试图摆脱的一种模式。一个问题是我们的代码必须跨平台,就我所知,pch 在 VC++ 和 gcc 中的工作方式不同。此外,我们最终得到了一个“n平方”包含复杂度,因为所有东西都包含在内,导致编译时间很长。我想我们可以将 STL 包含放在一个公共位置,但这意味着每个编译单元都会得到它们,无论它是否需要。我还注意到 STL 标头彼此包含,因此如果您包含(例如)'string',则可能不需要'memory'(只是一个例子)。 - user152154
2
我可以理解为什么你不想使用stdafx.h模式,但它确实可行,而且我认为这不是一个“糟糕的形式”。另外,我不建议把所有东西都放进stdafx头文件里——只放一些真正经常使用的头文件。.cpp文件仍然可以包含其他较不频繁需要的头文件在stdafx.h文件之后。我也不认为多次包含必要头文件是个问题——它们不应该对构建时间产生太大影响(文件被重复包含时不会被处理)。 - Michael Burr

2
我认为你应该仍然包含这两个文件。这有助于维护代码。
// Foo.cpp

#include <Library1>
#include <Library2>

我可以阅读并轻松看出它使用了哪些库。如果Library2使用了Library1,并且它被转换成了这个样子:
// Foo.cpp

#include <Library2>

但是我仍然看到了 Library1 的代码,这可能会让我有点困惑。虽然不难猜测其他的库可能正在引用它,但这仍然需要一定的思考过程。
明确表述可以避免任何猜测,甚至可以减少编译时间的微小延迟。

您是否也会将其扩展到STL库?例如,如果一个cpp文件使用“string”,您希望看到包含吗?在每个使用“string”的文件中都需要吗?我绝对可以理解您使用用户头文件的原因,但是查看我的代码,大多数源文件都必须在顶部包含一堆STL。 - user152154
2
等等,你还会用它们做什么?如果你的类在头文件中使用字符串,那么它应该放在那里,但如果只是实现中使用了它,那么它应该放在 cpp 文件中。每个 cpp 必须包含它所需的头文件。 - GManNickG

2

我认为你应该把两者都包含进来,直到变得难以忍受。然后重构你的类和源文件以减少耦合,因为如果列出所有的依赖很困难,那么你可能有太多的依赖关系...

妥协的做法可能是说,如果bar.h中的某些内容包含了一个必须需要qux.h中的另一个类定义(或函数声明),那么就可以假设/要求bar.h包含qux.h,并且在bar.h中使用API的用户不必同时包含两个文件。

所以,假设客户端想要把自己看作只对bar API感兴趣,但由于调用了一个通过值传递qux对象的bar函数,因此必须也使用qux时,它可以忘记qux有自己的头文件,并把它想象成定义在bar.h中的一个大的bar API,其中qux只是该API的一部分。

这意味着你不能指望,例如,搜索cpp文件中的qux.h提及来找到所有的qux客户端。但我从来不依赖于这一点,因为通常太容易意外地忽略已经间接包含的依赖项,从而没有列出所有相关的头文件。因此,在任何情况下都不应该假设头文件列表是完整的,而应该使用doxygen(或gcc -M,或其他工具)获取完整的依赖关系列表,并搜索它。


谢谢,这是一个很好的观点。Rampant包含确实可能是代码异味,过度耦合的迹象。我一定会仔细看看。 - user152154

1
如果foo.cpp包含bar.h和qux.h,但是bar.h本身包含qux.h怎么办?那么foo.cpp应该避免包含qux.h吗?
如果foo.cpp直接使用了qux.h中的任何内容,则应该自己包含此头文件。否则,由于bar.h需要qux.h,我会依赖于bar.h包含它所需的所有内容。

0

每个头文件都提供了使用某种类型服务所需的定义(类定义、一些宏等)。如果您直接使用通过头文件公开的服务,请包含该头文件,即使另一个头文件也包含它。另一方面,如果您仅间接地使用这些服务,只在其他头文件服务的上下文中使用,请不要直接包含头文件。

在我看来,无论哪种方式都不是很重要。第二次包含头文件不会被完全解析;除了第一次包含之外,它基本上被注释掉了。随着事情的变化,当您发现自己正在使用未直接包含的头文件时,添加它并不是什么大问题。


0

判断 foo.cpp 是否直接使用 qux.h 的一种方法是,思考如果 foo.cpp 不再需要包含 bar.h 会发生什么。如果 foo.cpp 仍然需要包含 qux.h,则应该显式列出它。如果它不再需要 qux.h 中的任何内容,则可能无需显式包含它。


0

我认为这里适用于“不要在没有剖析的情况下进行优化”的规则。

(这个答案偏向于“性能”而非“杂乱无章”;杂乱无章通常可以随意清理,尽管稍后变得更加困难,但性能会对您产生定期影响。)

尝试两种方式,看看编译器是否有任何显著的速度提升,然后再考虑删除“重复”的头文件——因为正如其他答案所指出的那样,通过删除重复的头文件,您将承担长期可维护性的惩罚。

考虑获取类似Sysinternals FileMon这样的工具,以查看是否实际上为这些重复包含生成了文件系统访问(大多数编译器不会,使用正确的头文件保护)。

我们的经验表明,积极寻找完全未使用的头文件(或者可以通过适当的前向声明来使用),并将它们删除,比确定可能重复的包含链路更值得花费时间和精力。而一个好的lint(splint,PC-Lint等)可以帮助您确定这一点。

更值得我们花费时间和精力的是找出如何让编译器在每次执行时处理多个编译单元(对我们来说几乎是线性加速,但到一定程度后编译被编译器启动所主导)。
之后,您可能想考虑“单个大 CPP”疯狂模式。这可能会非常令人印象深刻。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接