在 C++ 中使用双包含守卫
Posted
技术标签:
【中文标题】在 C++ 中使用双包含守卫【英文标题】:The use of double include guards in C++ 【发布时间】:2017-11-21 14:04:43 【问题描述】:所以我最近在我工作的地方进行了一次讨论,其中我质疑使用 双重 包含防护而不是单一防护。我所说的双重保护的意思如下:
头文件,“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
。同事告诉我,这为夹杂物增加了第二层保护,但如果第一层绝对能完成这项工作(或确实能做到?),我看不出第二层有什么用处。
我能想到的唯一好处是它完全阻止了链接器查找文件的麻烦。这是为了缩短编译时间(没有提到它的好处),还是这里有其他我没有看到的东西在起作用?
【问题讨论】:
这只是给代码增加了另一层脆弱性。第二层完全没有必要。 不是链接器,而是预处理器。老实说,如果你只包含你需要的东西,在现代构建系统上,任何这样的好处对我来说都是微不足道的。老实说,他的“解释”更像是一个专家初学者。 曾几何时,可能有一两个编译器愚蠢到每次都打开文件来检查包含保护。在这个千年中生产的编译器不会这样做,因为它可以只保留一个文件表并包含警卫并在打开文件之前查阅它。 完全没有必要。一点好处都没有。 请注意,包含两个连续下划线 (__HEADER_A_HPP__
) 的名称和以下划线后跟大写字母的名称保留供实现使用。不要在你的代码中使用它们。
【参考方案1】:
我很确定添加另一个包含防护是一种不好的做法,例如:
#ifndef __HEADER_A_HPP__
#include "header_a.hpp"
#endif
以下是一些原因:
为了避免双重包含,在头文件本身内添加一个常用的包含保护就足够了。它做得很好。在包含位置的另一个包含保护只会弄乱代码并降低可读性。
它增加了不必要的依赖。如果您在头文件中更改包含保护,则必须在包含头文件的所有位置进行更改。
与整个编译/链接过程相比,这绝对不是最昂贵的操作,因此几乎无法减少总构建时间。
任何有价值的编译器already optimizes file-wide include-guards。
【讨论】:
If you change include guard inside the header file you have to change it in all places where the header is included.
.... 好吧,从技术上讲,不,你不知道,但我认为这进一步证明了这一点。
我看到人们在尝试解决 Oracle Pro-C 预处理器的一些问题时会这样做。还是不喜欢。【参考方案2】:
在 header 文件中放置包含保护的原因是为了防止 header 的内容被多次拉入翻译单元。这是正常的、长期存在的做法。
在源代码文件中放置多余的包含保护的原因是为了避免打开包含的头文件,并且在过去可以显着加快编译速度。如今,打开文件比以前快得多了。此外,编译器非常聪明地记住他们已经看过哪些文件,并且他们理解包含保护习语,因此可以自己弄清楚他们不需要再次打开文件。这有点像在挥手,但最重要的是,不再需要这个额外的层了。
编辑:这里的另一个因素是编译 C++far 比编译 C 复杂,所以它需要 far 更长的时间,使得打开包含文件的时间更小,编译翻译单元所需的时间较少。
【讨论】:
这里有一个链接可以用一些文档来支持你的“挥手” ;-) : gcc.gnu.org/onlinedocs/cppinternals/Guard-Macros.html @ArneVogel 我注意到文档说:“在控制#if
-#endif
对之外不能有任何标记,但允许使用空格和 cmets。” “令牌”是否包括#pragma once
?
@sh3rifme 是的。将这句话读为“在不禁用优化的情况下,您可以放在控制 #if
-#endif
对之外的唯一内容是空格和 cmets。”但是you shouldn't be using #pragma once
anyway.
好吧,您可以将#pragma once
放在#ifndef/#endif
块内。但是我们不使用#pragma once
,因为我们在工作中使用的编译器之一不支持它。
@sh3rifme: #pragma once
不是令牌,而是预处理器指令。但是,这些也不允许优化工作。不过GCC支持#pragma once
,所以优化和#pragma once
是多余的。正如 Tom Tanner 建议的那样,如果您使用 both #pragma once
并包含守卫,则不妨将 pragma 放在 #ifndef/#endif
块内。在不太可能的情况下,您的编译器具有多个包含优化但不支持#pragma once
,这应该可以满足您的需求。也就是说,#include
-behavior 是出了名的依赖于实现。【参考方案3】:
我能想到的唯一好处是它完全阻止了链接器查找文件的麻烦。
链接器不会受到任何影响。
它可以防止 预处理器 费心去寻找文件,但是如果定义了守卫,那就意味着它已经找到了文件。我怀疑如果完全减少预处理时间,除了在最病理性递归包含的怪物中之外,效果将非常小。
它有一个缺点,如果守卫被更改(例如由于与另一个守卫冲突),则必须更改包含指令之前的所有条件以使其工作。如果其他东西使用了前面的守卫,那么必须更改条件以使包含指令本身正常工作。
附: __HEADER_A_HPP__
是为实现保留的符号,因此您不能定义它。为守卫使用另一个名字。
【讨论】:
对与链接器/预处理器的混淆表示歉意。你说__HEADER_A_HPP__
是保留给实现的,你这是什么意思?是不是专门使用了这些语义,比如math.hpp
和__MATH_HPP__
?
@sh3rifme 标准规定所有包含两个连续下划线的标识符都保留给实现。还有其他保留的标识符。我建议您熟悉这些规则。
@sh3rifme:保留给实现,可能包括当你包含header_a.hpp
时自动定义__HEADER_A_HPP__
这样的用法。这当然会破坏您的标题保护,假设它仅在 second 行上定义。【参考方案4】:
更传统(大型机)平台上的旧编译器(我们在这里谈论的是 2000 年代中期)过去没有在其他答案中描述过优化,因此它确实确实用于显着减慢预处理时间,不得不重新读取已经包含的头文件(请记住,在一个大型的、单一的、企业级的项目中,您将包含很多头文件)。例如,在 VisualAge C++ 6 for AIX 编译器(可以追溯到 2000 年代中期)上,我看到的数据表明,对于具有 256 个头文件的文件,每个头文件都包含相同的 256 个头文件,数据显示速度提高了 26 倍。这是一个相当极端的例子,但这种加速确实加起来了。
但是,所有现代编译器,甚至在 AIX 和 Solaris 等大型机平台上,都对包含头文件进行了足够的优化,以至于现在的差异实际上可以忽略不计。因此,没有充分的理由再拥有这些了。
然而,这确实解释了为什么一些公司仍然坚持这种做法,因为相对最近(至少在 C/C++ 代码库时代)它对于非常大的单体项目仍然是值得的。
【讨论】:
我记得曾经使用过 IBM fortran 编译器,它让蜗牛看起来像赛马。在相当强大的硬件上编译一个文件需要不少于半小时。gfortran
在很短的时间内完成了同样的工作。因此,也许 IBM 编译器并不是衡量编译速度的最佳参考。无论如何,在现代内核上,当编译器尝试读取它们时,这 256 个头文件仍将位于页面缓存中,因此如果系统调用小于 10 微秒,则在相同的 256 个文件上打开 64k 应该不超过一秒.
@cmaster 不仅是开头,还有读数——记住预处理器必须扫描到最后一个#endif
这也仍然在页面缓存中。即使 256 个文件中的每一个大小为 128 kiB,也只有 32 MiB 的数据,总共需要从内核空间复制到用户空间的 8 GiB 数据。现代硬件可以在不到一秒钟的时间内做到这一点。如果编译器在这个操作上花费了很长时间,那 100% 是编译器的错。【参考方案5】:
尽管有人反对它,但实际上“#pragma once”效果很好,主要编译器(gcc/g++、vc++)都支持它。
因此,无论人们传播什么纯粹的论点,它的效果都会好得多:
-
快速
无需维护,也不会因为您复制了旧标志而导致神秘的不包含问题
意义明显的单行与文件中散布的神秘行
简单地说:
#pragma once
在文件的开头,就是这样。经过优化、可维护且随时可用。
【讨论】:
包含守卫并不神秘。任何知道自己在做什么的 C(++) 程序员都会立即理解包含防护,而经验不足的程序员甚至可能需要查找#pragma once
(而他们可以使用标准的包含防护)。由于所有编译器都会优化包含保护(真正的编译器)或无法编译#pragma once
(玩具编译器),因此它也不比标准包含保护快。
@Kevin 但重点是守卫不容易出错,而且在 99.9% 的编译指示中,一次就足够了,这让您不必担心这个古老的知识。与 pragma once 相比——它们真的很神秘而且......很慢。 ;) 慢只是因为您需要编写、维护和阅读它们。
@Kevin 在使用 #ifdef 守卫 30 多年后,我很高兴我的同事 Kris 发现曾经到处都支持“新”编译指示。他写了一个脚本来批量替换它,虽然这只是一件小事,但这是让每个人都开心的事情。再次感谢克里斯!
@tom 你能说出几个没有的编译器吗?
#pragma once
was considered for standardisation, but rejected because it cannot be implemented reliably以上是关于在 C++ 中使用双包含守卫的主要内容,如果未能解决你的问题,请参考以下文章
在 Laravel 中使用多个守卫时,在注册/登录后设置默认守卫