依靠标头包含传递性是一种好习惯吗?


37

我正在清理正在处理的C ++项目中的包含,并且我一直在想是否应该将直接使用的所有标头明确包含在特定文件中,还是应该仅包含最低要求。

这是一个例子Entity.hpp

#include "RenderObject.hpp"
#include "Texture.hpp"

struct Entity {
    Texture texture;
    RenderObject render();
}

(让我们假设对它的前向声明RenderObject不是一种选择。)

现在,我知道其中RenderObject.hpp包括Texture.hpp-我知道,因为每个人RenderObject都有一个Texture成员。不过,我还是明确地将Texture.hppin 包含在内Entity.hpp,因为我不确定依赖于in是一个好主意RenderObject.hpp

因此:这是一种好的做法吗?


19
您的示例中的包含保护在哪里?您只是偶然地忘记了它们,希望吗?
Doc Brown

3
当您不包括所有使用过的文件时发生的一个问题是,有时您包括这些文件的顺序变得很重要。仅在发生这种情况的情况下,这确实很烦人,但有时会滚雪球,您最终还是希望像这样编写代码的人走在射击队的前面。
Dunk 2014年

这就是为什么#ifndef _RENDER_H #define _RENDER_H ... #endif
sampathsris,2014年

@Dunk我认为您误解了这个问题。有他的任何建议都不应该发生。
Mooing Duck

1
@DocBrown,#pragma once解决它,不是吗?
和平者

Answers:


65

无论您对这些文件中的内容有什么了解,都应始终包括所有标头,该标头定义该文件中.cpp文件中使用的任何对象。您应该在所有头文件中包含保护措施,以确保多次包含头都没有关系。

原因:

  • 对于清楚阅读源代码的开发人员来说,这很清楚,这是有关源文件的要求。在这里,查看文件前几行的人可以看到您正在处理Texture此文件中的对象。
  • 这避免了当重构的标头本身不再需要特定的标头本身时导致编译问题的问题。例如,假设您意识到RenderObject.hpp实际上并不需要Texture.hpp它。

必然的结果是,除非该文件中明确需要,否则不要在另一个标头中包含标头。


10
同意推论-附带条件,如果确实需要,则应始终包括其他标头!
2014年

1
我不喜欢直接为所有单个类包含标题的做法。我赞成使用累积标头。也就是说,我认为高级文件应该引用它正在使用的某种“模块”,但不必直接包含所有单个部分。
edA-qa mort-ora-y 2014年

8
这将导致每个文件都包含较大的整体头,即使它们只需要其中的一点点,也将导致较长的编译时间并使重构更加困难。
操纵机器人

6
Google建立了一个工具来帮助其准确执行此建议,称为include-what-you-use
马修·G。

3
大型整体标头的主要编译时间问题不是编译标头代码本身的时间,而是每次标头更改时都必须编译应用程序中的每个cpp文件。预编译的头文件无济于事。
装填了机器人

23

一般的经验法则是:包括您使用的内容。如果直接使用对象,则直接包含其头文件。如果您使用的对象A使用B,但您自己却不使用B,则仅包含Ah

同样,当我们在主题上时,仅应在头文件中实际需要时才在头文件中包括其他头文件。如果仅在.cpp中需要它,则仅在其中添加它:这是公共和私有依赖项之间的区别,并且将防止您的类用户拖入他们真正不需要的标头。


10

我一直在想我是否应该明确包括直接在特定文件中使用的所有标头

是。

您永远都不知道其他标头何时会更改。在每个翻译单元中都包含您知道翻译单元需要的标头,这在世界范围内都是有意义的。

我们有标题保护措施,以确保双重包含不会有害。


3

对此有不同的意见,但我认为每个文件(无论是c / cpp源文件还是h / hpp头文件)都应该能够自行编译或分析。

因此,所有文件都应#include他们需要的所有头文件-您不应该假定以前已包含一个头文件。

如果您需要添加头文件并发现它使用的是在其他地方定义的项目而没有直接包含它,那真是一件痛苦的事,因此您必须去查找(并且可能会得到错误的文件!)

另一方面,#include一个不需要的文件(通常)并不重要...


作为个人风格,我按字母顺序排列#include文件,分为系统和应用程序-这有助于加强“自包含且完全一致”的信息。


关于包含顺序的注意事项:有时顺序很重要,例如在包含X11标头时。这可能是由于设计(在这种情况下可能被认为是不良设计),有时是由于不幸的不兼容问题。
海德2014年

关于包含不必要的标头的说明,首先对编译时间很重要(特别是如果是大量模板的C ++),但是尤其是在包含相同或相关项目的标头(其中包含文件也发生变化)时,它会触发重新编译包括它在内的所有内容(如果您具有有效的依赖关系,如果没有,则必须一直进行干净的构建...)。
海德2014年

2

它取决于传递包含是否是必需的(例如,基类)还是由于实现细节(私有成员)而定。

为了明确起见,仅在首先更改中间标头中声明的接口之后,才可以在删除传递包含时将其包括在内。由于这已经是一个重大更改,因此无论如何都必须检查使用该文件的任何.cpp文件。

示例:Bh包含Ah,C.cpp使用。如果Bh在某些实现细节上使用了Ah,则C.cpp不应假定Bh将继续这样做。但是,如果Bh对基类使用Ah,则C.cpp可以假定Bh将继续为其基类包括相关的标头。

您将在此处看到不复制标头包含项的实际优势。假设Bh使用的基类确实不属于Ah,而是重构为Bh本身。Bh现在是独立的标头。如果C.cpp多余地包含Ah,则它现在包含不必要的头。


2

可能还有另一种情况:您有Ah,Bh和您的C.cpp,Bh包括Ah

所以在C.​​cpp中,您可以编写

#include "B.h"
#include "A.h" // < this can be optional as B.h already has all the stuff in A.h

因此,如果您在此处不写#include“ Ah”,会发生什么?在您的C.cpp中,同时使用了A和B(例如,类)。后来,您更改了C.cpp代码,删除了与B相关的内容,但在其中保留了Bh。

如果现在同时包括Ah和Bh,则此时检测到不必要的包含的工具可能会帮助您指出不再需要Bh包含。如果仅按上述方式包含Bh,则在代码更改后,工具/人员很难检测到不必要的包含。


1

我采用的方法与建议的答案略有不同。

在标头中,始终仅包括最低限度,即通过编译所需的最低限度。尽可能使用前向声明。

在源文件中,包含多少并不重要。我的喜好仍然包括使它通过的最低要求。

对于小型项目,在此处和在其中包含标题都不会有所不同。但是对于大中型项目,这可能会成为一个问题。即使使用最新的硬件进行编译,差异也很明显。原因是编译器仍然必须打开包含的标头并对其进行解析。因此,要优化构建,请应用上述技术(包括最低限度的要求,并使用前向声明)。

尽管有些过时,但是大型C ++软件设计(由John Lakos撰写)详细解释了所有这些。


1
不赞成这种策略……如果您在源文件中包含头文件,则必须跟踪其所有依赖项。最好直接包含,而不是尝试记录列表!
2014年

@Andrew提供了一些工具和脚本来检查其中包含的内容以及次数。
2014年

1
我已经注意到对某些最新的编译器进行了优化以解决此问题。他们识别出典型的警卫声明并进行处理。然后,再次包含它时,他们可以完全优化文件加载。但是,建议您使用前向声明来减少包含的数量是非常明智的。一旦开始使用前向声明,它将成为编译器运行时(由前向声明改善)和用户友好(由方便的额外#includes改善)之间的平衡,这是每个公司设置不同的平衡。
Cort Ammon 2014年

1
@CortAmmon一个典型的头有包括警卫,但是编译器还是要打开它,那就是运行速度慢
BЈовић

4
@BЈовић:实际上,他们没有。他们所需要做的就是认识到文件具有“典型”头保护并将其标记为仅打开一次。例如,Gcc拥有有关何时何地应用此优化的文档: gcc.gnu.org/onlinedocs/cppinternals/Guard-Macros.html
Cort Ammon

-4

优良作法是只要编译就不要担心标头策略。

代码的标题部分只是一行代码,在您得到易于解决的编译错误之前,任何人都不要看。我了解对“正确”样式的渴望,但没有一种方法可以真正描述为正确。在每个类中都包含标头很可能会引起恼人的基于订单的编译错误,但这些编译错误也反映出了可以通过仔细编码解决的问题(尽管可以说它们不值得花时间修复)。

是的,一旦您开始着陆,您遇到那些基于订单的问题friend

您可以在两种情况下想到该问题。


情况1:您有少量的类彼此交互,例如少于十二个。 您定期添加,删除或以其他方式修改这些标头,这可能会影响它们彼此之间的依赖性。您的代码示例建议这种情况。

标头集足够小,因此解决任何出现的问题并不复杂。任何困难的问题都可以通过重写一个或两个标题来解决。担心头策略会解决不存在的问题。


情况2:您有数十个课程。 有些类代表程序的主干,重写它们的标头将迫使您重写/重新编译大量的代码库。其他类使用此主干来完成任务。这代表了典型的业务环境。标头分布在目录中,您实际上无法记住所有名称。

解决方案:在这一点上,您需要考虑逻辑组中的类,并将这些组收集到标头中,以免您不得不#include一遍又一遍。这不仅简化了工作,而且是利用预编译标头的必要步骤。

您最终#include会上不需要的课,但谁在乎呢?

在这种情况下,您的代码将看起来像...

#include <Graphics.hpp>

struct Entity {
    Texture texture;
    RenderObject render();
}

13
我不得不将其设为-1,因为老实说我相信任何形式为“良好实践都不必担心您的____策略,只要它能够编译”的句子都会使人做出错误的判断。我发现这种方法非常迅速地导致了不可读取性,不可读取性被称为“行不通”。我还发现了许多主要的库,它们与您描述的两种情况的结果都不相同。例如,Boost DOES会执行您在情况2中推荐的“ collections”标头,但是它们在为您需要时提供逐类标头方面也大有作为。
Cort Ammon 2014年

3
我亲眼目睹了“不要担心它是否会编译”变成“当您向枚举添加值时,我们的应用程序需要30分钟的编译时间,我们该怎么解决!”
Gort机器人

我在回答中谈到了编译时间的问题。实际上,我的回答是仅有的两个(均得分不高)之一。但这确实与OP的问题有关;这是“我应该用驼峰大小写变量名吗?” 输入问题。我意识到我的回答不受欢迎,但是对于所有问题并不总是有最佳实践,这就是其中一种情况。
QuestionC

同意#2。至于早期的想法-我希望可以更新本地标头块的自动化-在此之前,我主张一个完整的列表。
chux-恢复莫妮卡2015年

“包括所有内容和厨房水槽”方法最初可能会为您节省一些时间-您的头文件甚至看起来更小(因为大多数内容是从...某处间接包含的)。直到您发现任何地方的任何更改都会导致30分钟以上的项目重新完成。您的IDE智能自动完成功能会提出数百条不相关的建议。而且您不小心混淆了两个名称相似的类或静态函数。然后添加新的结构,但是构建失败,因为您的命名空间与某个完全不相关的类发生冲突……
CharonX
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.