C++中使用双重包含保护的作用。

73

最近我在工作中讨论了一个问题,我质疑使用双重包含保护是否比单重更好。我所说的双重包含保护如下:

头文件 "header_a.hpp":

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

当在头文件或源文件中包含头文件时:

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

现在我明白了,在头文件中使用守卫的作用是防止重复包含已经定义过的头文件,这是很常见并且有良好的文档说明。如果宏已经被定义,整个头文件将被编译器视为“空白”,以防止重复包含。很简单。

问题是我不理解在 #include "header_a.hpp"周围使用#ifndef __HEADER_A_HPP__#endif。我的同事告诉我,这样做可以添加第二层保护来防止重复包含,但我无法理解如果第一层绝对能够完成工作,那么第二层的作用是什么(或者有吗?)。

唯一想到的好处就是彻底停止链接器打扰文件。这是否意味着这可以提高编译时间(这并没有被提及为一个好处),或者有其他我看不到的东西起作用?


36
这只是为代码增加了另一层脆弱性。第二层是完全不必要的。 - DeiDei
19
不是链接器,而是预处理器。老实说,在现代构建系统中,只要包含所需内容,任何这样的好处对我来说似乎微不足道。他的“解释”老实说更像是一个初学者。 - StoryTeller - Unslander Monica
18
从前,可能有一两个编译器够傻以至于每次都要打开文件来检查 include guard。但是,在本千年生产的任何一个编译器都不会这样做,因为它可以只需维护一个文件和 include guard 的表格,在打开文件之前先查询该表格。 - Bo Persson
7
完全没有必要。毫无益处。 - Jabberwocky
34
请注意,包含两个连续下划线(__HEADER_A_HPP__)的名称以及以下划线和大写字母开头的名称都被保留供实现使用。请勿在您的代码中使用它们。 - Pete Becker
显示剩余16条评论
5个回答

107

我非常确定,像这样添加另一个包含保护是一种不好的做法:

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

以下是一些原因:

  1. 为了避免重复包含,只需在头文件中添加一个常规的include guard即可。它可以很好地完成工作。在包含的位置加另一个include guard只会混乱代码并降低可读性。

  2. 它增加了不必要的依赖性。如果你在头文件内更改了include guard,你必须在所有包含该头文件的地方进行更改。

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

  4. 任何值得一提的编译器 已经优化了全局的include-guards


2
如果您在头文件中更改了包含保护,那么您必须在所有包含该头文件的地方进行更改。嗯,从技术上讲,您并不需要这样做,但我认为这进一步证明了这一点。 - txtechhelp
我曾经看到有人这样做,试图绕过Oracle Pro-C预处理器的一些问题。但我仍然不喜欢它。 - user2038893

51
在头文件中加入include guards的原因是为了防止头文件的内容被多次引入到翻译单元中。这是一种正常、长期以来的做法。
在源文件中放置冗余的include guards的原因是避免打开正在被包含的头文件,在旧时代,这可以显著加快编译速度。现在,打开一个文件比以前快得多;此外,编译器非常聪明,它们记住已经看过哪些文件,并且理解include guard惯用语,所以可以自行判断它们不需要再次打开文件。这有点像手挥舞,但底线是这个额外的层次不再需要了。
编辑:另一个因素是编译C++比编译C要复杂得多,因此需要更长的时间,这使得打开包含文件的时间成为编译翻译单元所需时间的较小、不太重要的部分。

7
这是一份关于“手势舞蹈”背后的一些文档链接,可以帮助您进行补充说明;-): https://gcc.gnu.org/onlinedocs/cppinternals/Guard-Macros.html - Arne Vogel
@ArneVogel 我注意到文档中说:“控制#if-#endif对之外不能有任何标记,但允许使用空格和注释。”这里的“标记”是否包括#pragma once - sh3rifme
3
@sh3rifme,是的。将那个句子理解为:“只有空格和注释可以放在控制#if-#endif对之外而不会禁用优化。”但是,你其实不应该使用#pragma once(参见https://dev59.com/gXM_5IYBdhLWcg3w-4dg#34884735)。 - zwol
好的,你可以将 #pragma once 放在 #ifndef/#endif 块内。但是我们不使用 #pragma once,因为我们工作中使用的编译器之一不支持它。 - Tom Tanner
@sh3rifme: #pragma once 不是一个标记,而是一个预处理器指令。这些指令也不允许进行优化。然而,GCC支持 #pragma once,所以优化和 #pragma once 是多余的。正如Tom Tanner所建议的那样,如果你同时使用 #pragma once 和包含保护,你可以将 pragma 放在 #ifndef/#endif 块内部。在极少数情况下,如果你的编译器具有多重包含优化但不支持 #pragma once,那么这应该可以解决问题。话虽如此,#include 的行为通常取决于实现。 - Arne Vogel

22
我能想到的唯一好处就是它可以直接阻止链接器去寻找这个文件。
链接器不会受到任何影响。
它可能会防止预处理器找到该文件,但如果定义了宏保护符,则意味着已经找到该文件。我怀疑即使在最病态的递归包含怪物中,如果预处理时间有所减少,效果也会相当微小。
它的缺点是,如果宏保护符被更改(例如与其他宏保护符冲突),则必须更改所有包含指令之前的条件语句才能使它们正常工作。如果其他内容使用先前的宏保护符,则必须更改条件语句才能使包含指令本身正常工作。
附注:__HEADER_A_HPP__ 是一个保留给实现的符号,因此您不能定义它。请使用其他名称作为宏保护符。

对于链接器/预处理器的混淆表示歉意。您说__HEADER_A_HPP__是保留给实现的,这是什么意思?是否特别使用那些语义是问题所在,例如math.hpp__MATH_HPP__ - sh3rifme
13
标准规定,所有包含两个连续下划线的标识符都保留给实现。还有其他一些保留的标识符。我建议你熟悉这些规则。 - eerorika
7
保留给实现,可能包括这样的用法,例如在包含“header_a.hpp”时自动定义“__HEADER_A_HPP__”。当然,这会破坏您的头文件保护,因为它假定该标识符只在第二行上定义。 - MSalters

17

早期传统平台上(指2000年代中期左右),较老的编译器没有其他答案所述的优化,因此重新读取已被包含的头文件确实会显著降低预处理时间(请记住,在一个大型、单块式、企业级项目中,您将包含大量的头文件)。例如,根据数据显示,在 VisualAge C++ 6 for AIX 编译器上(该编译器来自2000年代中期),对于一个包含256个相同头文件的文件,每个头文件都包含相同的256个头文件,可以提高26倍速度。这是一个相当极端的例子,但这种加速确实会累积起来。

然而,即使在AIX和Solaris等主机平台上,所有现代编译器都会执行足够的头文件包含优化,以至于现在的差异真的是微不足道的。因此,没有好的理由再继续使用这些东西。

然而,这解释了为什么一些公司仍然坚持这种做法,因为在很大的单块式项目中,最近(至少从C/C++代码库年龄的角度来看)仍然是值得的。


我记得曾经不得不使用IBM Fortran编译器,它让蜗牛看起来像赛马。一个文件在相当强大的硬件上编译需要不少于半个小时。gfortran在一小部分时间内完成了同样的工作。因此,也许IBM编译器不是衡量编译速度的最佳参考。无论如何,在现代内核上,当编译器尝试读取它们时,这256个头文件仍将位于页面缓存中,因此对同256个文件进行64k打开操作应该不超过1秒,如果系统调用小于10微秒。 - cmaster - reinstate monica
1
@cmaster 不仅是开头,还有读取 - 记住预处理器必须扫描到最后一个 #endif。 - Muzer
2
即使这256个文件中的每一个都是128 kiB大小,它们仍然在页面缓存中。这只是32 MiB的数据,总共需要将8 GiB的数据从内核空间复制到用户空间。现代硬件可以在不到一秒钟的时间内完成这项工作。如果编译器在此操作上花费了很长时间,那么这完全是编译器的问题。 - cmaster - reinstate monica

8
虽然有人对此提出异议,但在实践中,“#pragma once”完美工作,并得到主要编译器(gcc/g++、vc++)的支持。
所以无论人们传播怎样的纯粹主义论点,它都能更好地发挥作用:
1. 快速 2. 无需维护,不会因为复制旧标记而导致神秘的未包含问题 3. 单行明显含义,与散布在文件中的难以理解的代码相比,易于理解
简而言之:
#pragma once

在文件开头加上这段代码,就行了。这样做可以优化代码、易于维护,而且准备工作也完成了。

1
包含保护并不神秘。任何了解自己在做什么的C(++)程序员都会立即理解包含保护,而经验不足的人甚至可能需要查找#pragma once(而他们使用标准包含保护就可以了)。由于所有编译器都将优化包含保护(真正的编译器)或无法编译#pragma once(玩具编译器),因此它也不比标准包含保护更快。 - Kevin
4
但是重点是守卫不容易出错,在99.9%的情况下,#pragma once已经足够让你不必担心这个老问题了。与#pragma once相比,它们确实更加晦涩和缓慢。只是因为你需要编写、维护和阅读它们才会变慢。 - Dmitry Azaraev
1
@Kevin 在使用 #ifdef 保护符号30多年后,我很高兴我的同事Kris发现了'new' pragma once现在已经被广泛支持。他编写了一个脚本来批量替换它,虽然这只是一个小事情,但这是让每个人都感到高兴的事情。再次感谢Kris! - Bert Bril
@tom,你能列举一些不这样做的编译器吗? - Bert Bril
1
#pragma once 被考虑用于标准化,但被拒绝了,因为它不能可靠地实现。 - Kos
@Kos 我理解你的观点。看看Visual C++强制我们使用所有dll_export恐怖和其他东西。但是,#pragma once确实在我们支持的所有平台上都可以可靠地实现跨平台,并且在任何时候我们将合理支持它。如果在任何时候会出现问题...那么...编写一个脚本以生成ifdefs以替换#pragma语句很容易。 - Bert Bril

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