在C ++中使用双重包含保护


73

因此,我最近在我的工作地点进行了讨论,其中我质疑使用双重包含保护而不是单个保护。我所说的双重保护如下:

头文件“ header_a.hpp”:

#ifndef __HEADER_A_HPP__
#define __HEADER_A_HPP__
...
...
#endif

在头文件或源文件中的任何位置包含头文件时:

#ifndef __HEADER_A_HPP__
#include "header_a.hpp"
#endif

现在,我知道在头文件中使用防护是为了防止多次包含已经定义的头文件,这是常见且有据可查的。如果已经定义了宏,则编译器会将整个头文件视为“空白”,并防止重复包含。很简单。

我不理解的问题是使用#ifndef __HEADER_A_HPP__#endif周围#include "header_a.hpp"。我的同事告诉我,这为夹杂物增加了第二层保护,但是我看不到如果第一层确实完成了工作(或做到了吗?),那么第二层是多么有用。

我能想到的唯一好处是,它彻底阻止了链接程序费心查找文件。这是否是为了缩短编译时间(这没有被提及是有好处的),还是还有其他我看不到的东西在起作用?


36
这只是给代码增加了另一层脆性。第二层是完全不必要的。
DeiDei

19
不是链接器,而是预处理器。老实说,如果仅包含您需要的内容,那么在现代构建系统上,任何此类好处对我而言都是微不足道的。老实说,他的“解释”让人想起专家初学者。
StoryTeller-Unslander Monica

18
曾几何时,可能有一个或两个愚蠢的编译器每次都打开该文件以检查include防护。在这个千年中,没有一个编译器可以做到这一点,因为它只能保存一个文件表并包含防护措施,并在打开文件之前进行查询。
Bo Persson

7
完全没有必要。根本没有好处。
Jabberwocky

34
请注意,包含两个连续下划线(__HEADER_A_HPP__)的名称以及以下划线后跟大写字母开头的名称保留供实现使用。不要在您的代码中使用它们。
皮特·贝克尔

Answers:


107

我敢肯定,添加另一个包含后卫的做法是错误的做法:

#ifndef __HEADER_A_HPP__
#include "header_a.hpp"
#endif

原因如下:

  1. 为了避免重复包含,只需在头文件本身内添加一个常规的包含保护即可。它做得很好。另一个包含替代项的include防护只会使代码混乱并降低可读性。

  2. 它增加了不必要的依赖性。如果在头文件中更改包含保护,则必须在包含头的所有位置进行更改。

  3. 比较整个编译/链接过程,绝对不是最昂贵的操作,因此它几乎不会减少总构建时间。

  4. 任何有价值的编译器 已经可以优化整个文件的include-guards


2
If you change include guard inside the header file you have to change it in all places where the header is included.....从技术上讲,不是的,但是我认为这种观点进一步证明了这一点。
txtechhelp

我已经看到人们在尝试解决Oracle Pro-C预处理程序的某些问题时会这样做。还是不喜欢。
user2038893

51

文件中放置包含保护的原因是为了防止将头的内容多次拉入翻译单元。这是正常的,长期存在的做法。

文件中放置冗余包含防护的原因是避免不得不打开要包含的头文件,而在较早的年代可以大大加快编译速度。如今,打开文件的速度比以前快得多。此外,编译器非常聪明地记住他们已经看过的文件,并且他们了解include Guard习惯用法,因此可以自己弄清楚自己不需要再次打开该文件。有点麻烦,但是最重要的是不再需要这个额外的图层。

编辑:这里的另一个因素是,编译C ++是远远复杂得多,编译C,所以它需要远远更长的时间,使花开放的时间包含文件需要编译转换单元中,时间更小,更显著部分。


7
这是备份您的“挥手”和一些文档的链接;-):gcc.gnu.org/onlinedocs/cppinternals/Guard-Macros.html
Arne Vogel,

@ArneVogel我注意到文档说:“控制#if-#endif配对之外必须没有令牌,但允许空格和注释。” “标记”包括#pragma once吗?
sh3rifme

3
@ sh3rifme是的。将该句子读为“您可以在控制之外放置的唯一内容#if-#endif对而不禁用优化是空白和注释。” 但是无论如何你都不应该使用#pragma once
zwol

好吧,你可以把它#pragma once放进去#ifndef/#endif。但是我们不使用,#pragma once因为我们在工作中使用的其中一个编译器不支持它。
汤姆·坦纳

@ sh3rifme:#pragma once不是令牌,而是预处理程序指令。但是,这些也不允许优化工作。但是,GCC支持#pragma once,因此优化和#pragma once是多余的。当汤姆坦纳建议,如果使用两个 #pragma once,包括警卫,你还不如把编译里面#ifndef/#endif块。在不太可能的情况下,您的编译器具有多个包含优化但不支持#pragma once,这应该可以解决。就是说,-#include行为反正是依赖于实现的。
Arne Vogel

22

我能想到的唯一好处是,它可以彻底阻止链接程序费心查找文件。

链接器不会受到任何影响。

它可以防止预处理程序费心查找文件,但是,如果定义了防护措施,则意味着它已经找到了文件。我怀疑,如果完全减少了预处理时间,那么除了在病理上最递归地包含怪物的情况之外,其影响将是很小的。

不利的一面是,如果曾经更改过防护(例如,由于与另一个防护发生冲突),则必须更改include指令之前的所有条件,以使其起作用。如果其他东西使用了先前的保护措施,则必须更改条件,以使include指令本身能够正常工作。

PS__HEADER_A_HPP__是为实现保留的符号,因此您不能定义它。为卫队使用另一个名称。


对于与链接器/预处理器的混淆感到抱歉。您说这__HEADER_A_HPP__是保留给实现的,这是什么意思?是否专门使用了像math.hppand这样的语义__MATH_HPP__
sh3rifme

13
@ sh3rifme标准表示,包含两个连续下划线的所有标识符都保留给实现。还有其他保留的标识符。我建议您熟悉这些规则。
eerorika

7
@ sh3rifme:保留给实现,其中可能包括诸如自动定义__HEADER_A_HPP__何时包含的用法header_a.hpp。当然,这会破坏您的标头后卫,假设它仅在第二行中定义。
MSalters

17

在更传统的(大型机)平台上的较旧的编译器(我们在这里指的是2000年代中期)不曾具有其他答案中描述的优化,因此它确实确实大大降低了必须重新读取头文件的预处理时间。已经包含在内(请记住,在一个大型的,整体的企业级项目中,您将包含很多头文件)。作为示例,我看到的数据表明,具有256个头文件的文件的速度提高了26倍,每个头文件在AIX编译器的VisualAge C ++ 6上可追溯到2000年代中期,每个头文件包括相同的256个头文件。这是一个极端的例子,但是这种提速确实加起来。

但是,所有现代编译器,即使在诸如AIX和Solaris之类的大型机平台上,也执行了足够的优化以包含标头,这使得如今的差异确实可以忽略不计。因此,没有充分的理由再拥有这些。

但是,这确实解释了为什么有些公司仍然坚持这种做法,因为相对最近(至少在C / C ++代码库时代而言),对于大型的整体项目仍然值得。


我记得曾经不得不使用一次IBM fortran编译器,它使蜗牛看上去像是赛马。在强大的硬件上编译一个文件至少要花费半小时。gfortran在那段时间内完成了相同的工作。因此,也许IBM编译器不是衡量编译速度的最佳参考。无论如何,在现代内核上,当编译器尝试读取它们时,那​​些256个头文件仍将保留在页面缓存中,因此,如果syscall小于10微秒,则在同一256个文件上打开64k应该不超过一秒钟。 。
cmaster-恢复莫妮卡

1
@cmaster不仅是开头,而且还有阅读-记住预处理器必须扫描到最后的#endif
Muzer

2
这也仍然在页面缓存中。即使256个文件中的每个文件的大小为128 kiB,也只有32 MiB的数据,总计需要从内核空间复制到用户空间8 GiB的数据。现代硬件可以在不到一秒钟的时间内完成此操作。如果编译器花费很长时间进行此操作,那是100%的编译器故障。
cmaster-恢复莫妮卡

8

尽管有人对此表示反对,但实际上“ #pragma一旦”可以完美地工作,并且主要的编译器(gcc / g ++,vc ++)都支持它。

因此,无论人们在传播什么纯粹主义的论点,它的效果都会更好:

  1. 快速
  2. 无需维护,神秘的不包含项不会带来麻烦,因为您复制了旧标记
  3. 具有明显含义的单行与文件中散布的神秘行

简而言之:

#pragma once

在文件的开头,仅此而已。优化,可维护,随时可用。


1
包含防护不是神秘的。任何知道自己在做什么的C(++)程序员都将立即了解一个include防护,而经验不足的程序员甚至可能需要查找#pragma once(而使用标准的include防护就可以了)。由于所有编译器都会优化includeguard(实际编译器)或无法编译#pragma once(玩具编译器),因此它的速度也不比标准的includeguard快。
凯文(Kevin)

3
@Kevin但主要的一点是,后卫并不容易出错,并且99.9%的实用率一次就足够了,这使您不必担心这件事。与一次编译指示相比,它们确实是神秘的,而且...很慢。;)仅因为需要编写,维护和阅读而变慢。
德米特里·阿扎拉耶夫

1
@Kevin使用#ifdef护卫30多年后,我很高兴我的同事Kris意识到现在曾经在任何地方都支持“新”实用主义。他写了一个脚本来批量替换它,尽管这只是一件小事,但这是使每个人都高兴的事情。再次感谢Kris!
Bert Bril

@tom您能说出一些没有的编译器吗?
Bert Bril

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.