【问题标题】:Is #pragma once a safe include guard?#pragma once 是安全的包含守卫吗?
【发布时间】:2010-10-21 17:07:06
【问题描述】:

我了解到在使用 #pragma once 时有一些编译器优化,可以加快编译速度。我知道这是非标准的,因此可能会造成跨平台兼容性问题。

非 Windows 平台 (gcc) 上的大多数现代编译器都支持这种功能吗?

我想避免平台编译问题,但也想避免后备守卫的额外工作:

#pragma once
#ifndef HEADER_H
#define HEADER_H

...

#endif // HEADER_H

我应该担心吗?我是否应该为此花费更多的精力?

【问题讨论】:

  • 在询问了similar question 之后,我发现#pragma once 似乎避免了 VS 2008 中的一些类视图问题。我正在摆脱包含守卫并将它们全部替换为#pragma once 出于这个原因。

标签: c++ include-guards


【解决方案1】:

#pragma once 确实有一个缺点(除了非标准),那就是如果你在不同的位置有相同的文件(我们有这个,因为我们的构建系统复制文件)然后编译器会认为这些是不同的文件。

【讨论】:

  • 但是你也可以在不同的位置拥有两个同名的文件,而不必费心创建不同的#define NAMES,这通常是HEADERFILENAME_H的形式
  • 您还可以拥有两个或多个具有相同#define WHATEVER 的文件,这会带来无穷无尽的乐趣,这就是我倾向于使用 pragma 一次的原因。
  • 没有说服力...将构建系统更改为不复制文件而是使用符号链接的构建系统,或者仅从每个翻译单元的一个位置包含相同的文件。听起来您的基础架构更像是一团糟,必须重新组织。
  • 如果你在不同的目录中有不同的同名文件,#ifdef 方法会认为它们是同一个文件。所以一个有缺点,另一个也有缺点。
  • @rxantos,如果文件不同,#ifdef 宏值也可能不同。
【解决方案2】:

使用#pragma once 应该适用于任何现代编译器,但我认为没有任何理由不使用标准的#ifndef 包含防护。它工作得很好。需要注意的是,GCC 在version 3.4 之前不支持#pragma once

我还发现,至少在 GCC 上,it recognizes the standard #ifndef include guard and optimizes it,所以它不应该比 #pragma once 慢很多。

【讨论】:

  • 它应该不会更慢(无论如何使用 GCC)。
  • 不是这样实现的。相反,如果文件第一次以#ifndef 开头并以#endif 结尾,gcc 会记住它并在以后总是跳过该包含,甚至无需打开文件。
  • #pragma once 通常更快,因为文件没有被预处理。 ifndef/define/endif 无论如何都需要预处理,因为在这个块之后你可以有一些可编译的东西(理论上)
  • GCC docs on the guard 宏优化:gcc.gnu.org/onlinedocs/cppinternals/Guard-Macros.html
  • 要使用包含保护,还有一个额外的要求是您必须定义一个新符号,例如#ifndef FOO_BAR_H,通常用于诸如“foo_bar.h”之类的文件。如果您稍后重命名此文件,是否应该相应地调整包含保护以符合此约定?此外,如果您在代码树的两个不同位置有两个不同的 foo_bar.h,则必须为每一个考虑两个不同的符号。简短的回答是使用#pragma once,如果您确实需要在不支持它的环境中编译,那么请继续为该环境添加包含防护。
【解决方案3】:

我希望#pragma once(或类似的东西)已经在标准中。包含警卫并不是什么大问题(但它们似乎确实有点难以向学习该语言的人解释),但这似乎是一个可以避免的小烦恼。

事实上,由于 99.98% 的时间,#pragma once 行为是期望的行为,如果防止多个头文件的包含由编译器自动处理,使用 #pragma 或其他东西会很好允许双重包括。

但我们拥有我们所拥有的(除了您可能没有#pragma once)。

【讨论】:

  • 我真正想要的是一个标准的#import 指令。
  • 一个标准的导入指令即将到来:isocpp.org/blog/2012/11/… 但还没有。我强烈支持它。
  • @AHelps Vaporware。到现在已经快五年了。也许在 2023 年你会回到这条评论并说“我告诉过你”。
  • 应该把它变成C++20。
  • ... 并已将其转化为 C++20。
【解决方案4】:

我不知道任何性能优势,但它确实有效。我在所有 C++ 项目中都使用它(当然我使用的是 MS 编译器)。我发现它比使用更有效

#ifndef HEADERNAME_H
#define HEADERNAME_H
...
#endif

它做同样的工作,不会用额外的宏填充预处理器。

GCC 正式支持#pragma once as of version 3.4

【讨论】:

    【解决方案5】:

    GCC 从 3.4 开始支持 #pragma once,请参阅 http://en.wikipedia.org/wiki/Pragma_once 以获取更多编译器支持。

    我看到使用 #pragma once 而不是包含守卫的最大好处是避免复制/粘贴错误。

    让我们面对现实吧:我们大多数人几乎不会从头开始创建新的头文件,而只是复制现有的头文件并根据需要对其进行修改。使用 #pragma once 而不是包含守卫创建工作模板要容易得多。我修改模板的次数越少,出错的可能性就越小。在不同的文件中使用相同的包含保护会导致奇怪的编译器错误,并且需要一些时间才能找出问题所在。

    TL;DR:#pragma once 更易于使用。

    【讨论】:

      【解决方案6】:

      我使用它并且对它很满意,因为我需要输入更少的内容来创建一个新的标题。它在三个平台上运行良好:Windows、Mac 和 Linux。

      我没有任何性能信息,但我相信与解析 C++ 语法的速度相比,#pragma 和 include 保护之间的区别将是微不足道的。这才是真正的问题。例如,尝试使用 C# 编译器编译相同数量的文件和行,以查看差异。

      最后,使用守卫或pragma,根本不重要。

      【讨论】:

      • 我不喜欢#pragma once,但我感谢您指出相对的好处...在“正常”操作环境中,C++ 解析比其他任何东西都要昂贵得多。如果编译时间是个问题,没有人会从远程文件系统编译。
      • Re C++ 解析慢与 C#。在 C# 中,您不必为每个微小的 C++ 文件解析(字面意思)数千个 LOC 头文件(iostream,有人吗?)。然而,使用预编译的头文件可以使这个问题更小
      【解决方案7】:

      使用 '#pragma once' 可能没有任何效果(并非所有地方都支持它——尽管它越来越广泛地被支持),所以无论如何你都需要使用条件编译代码,在这种情况下,为什么还要使用 '@987654322 @'?编译器可能无论如何都会对其进行优化。不过,它确实取决于您的目标平台。如果你所有的目标都支持它,那么继续使用它——但这应该是一个有意识的决定,因为如果你只使用 pragma 然后移植到不支持它的编译器,所有的地狱都会崩溃。

      【讨论】:

      • 我不同意你无论如何都必须支持警卫。如果您使用#pragma once(或guards),这是因为没有它们会引发一些冲突。因此,如果您的链工具不支持该项目,则该项目将无法编译,并且您的情况与您想在旧的 K&R 编译器上编译一些 ansi C 时完全相同。您只需要获得更新的链工具或更改代码以添加一些防护。如果程序正在编译但无法运行,那么一切都会崩溃。
      【解决方案8】:

      性能优势在于一旦读取 #pragma once 就不必重新打开文件。使用警卫,编译器必须打开文件(这可能会花费大量时间)以获取不应再次包含其内容的信息。

      这只是理论上的,因为某些编译器不会为每个编译单元自动打开没有任何读取代码的文件。

      无论如何,并非所有编译器都是如此,因此理想情况下,跨平台代码必须避免使用 #pragma once,因为它根本不是标准的/没有标准化的定义和效果。但是,实际上,它确实比守卫好。

      最后,better suggestion you can get 确保在这种情况下无需检查每个编译器的行为即可确保从编译器中获得最佳速度,即同时使用 pragma once 和警卫。

      #ifndef NR_TEST_H
      #define NR_TEST_H
      #pragma once
      
      #include "Thing.h"
      
      namespace MyApp
      {
       // ...
      }
      
      #endif
      

      这样您就可以充分利用两者(跨平台和帮助编译速度)。

      由于打字时间较长,我个人使用一种工具来帮助以一种非常棒的方式生成所有内容(Visual Assist X)。

      【讨论】:

      • Visual Studio 不按原样优化#include 保护吗?其他(更好的?)编译器这样做,我想这很容易。
      • 为什么要把pragma放在ifndef之后?有什么好处吗?
      • @user1095108 一些编译器将使用标头保护作为分隔符来了解文件是否仅包含必须实例化一次的代码。如果某些代码在标头保护之外,那么整个文件可能会被认为可以多次实例化。如果同一个编译器一次不支持 pragma,那么它将忽略该指令。因此,将 pragma once 放在标头保护中是确保至少可以“优化”标头保护的最通用方法。
      【解决方案9】:

      并非总是如此。

      http://gcc.gnu.org/bugzilla/show_bug.cgi?id=52566 有一个很好的例子,两个文件都被包含在内,但由于时间戳和内容相同(文件名不同)而被错误地认为是相同的。

      【讨论】:

      • 那将是编译器中的一个错误。 (试图走一条不该走的捷径)。
      • #pragma once 是非标准的,所以无论编译器决定做什么都是“正确的”。当然,接下来我们就可以开始谈什么是“预期”,什么是“有用”。
      【解决方案10】:

      在非常大的树上使用 gcc 3.4 和 4.1(有时使用 distcc),我还没有看到使用 #pragma 代替标准包含防护或与标准包含防护结合使用时有任何加速。

      我真的不明白它有多么值得混淆旧版本的 gcc,甚至其他编译器,因为没有真正的节省。我还没有尝试过所有各种 delinter,但我敢打赌这会让他们中的许多人感到困惑。

      我也希望它早日被采用,但我可以看到“当 ifndef 工作得非常好时,为什么我们需要它?”的论点。鉴于 C 的许多黑暗角落和复杂性,包含守卫是最简单、自我解释的事情之一。如果您对预处理器的工作原理有一点了解,它们应该是不言自明的。

      但是,如果您确实观察到显着加速,请更新您的问题。

      【讨论】:

        【解决方案11】:

        今天,老式的包含警卫与#pragma once 一样快。即使编译器没有特别对待它们,它仍然会在看到 #ifndef WHATEVER 和 WHATEVER 被定义时停止。今天打开文件非常便宜。即使有改进,也应该是毫秒级的。

        我只是不使用#pragma 一次,因为它没有任何好处。为了避免与其他包含守卫发生冲突,我使用如下内容: CI_APP_MODULE_FILE_H --> CI = Company Initials; APP = 应用程序名称;其余的不言自明。

        【讨论】:

        • 减少打字的好处不是吗?
        • 请注意,几毫秒十万次是几分钟。大型项目由数万个文件组成,每个文件包括数十个标头。鉴于当今的众核 CPU,输入/输出,尤其是打开许多小文件,是主要瓶颈之一。
        • “如今,老式的 include 守卫与 #pragma 一次一样快。” 今天,也是很多年前。 GCC 网站上最古老的文档是针对 2001 年的 2.95 的,当时优化包含守卫并不新鲜。这不是最近的优化。
        • 主要的好处是包含守卫容易出错且冗长。在不同的目录中有两个具有相同名称的不同文件太容易了(并且包含保护可能是相同的符号),或者在复制包含保护时出现复制粘贴错误。 Pragma once 不易出错,适用于所有主要的 PC 平台。如果你能用它,它是更好的风格。
        • 今天打开文件太便宜了。哦呵呵呵呵。尝试从网络安装构建一个大型项目:)
        【解决方案12】:

        主要区别在于编译器必须打开头文件才能读取包含保护。相比之下,pragma once 会导致编译器跟踪文件,并且在遇到同一文件的另一个包含时不执行任何文件 IO。虽然这听起来可以忽略不计,但它可以轻松扩展大型项目,尤其是那些没有良好标头包含学科的项目。

        也就是说,如今的编译器(包括 GCC)已经足够聪明,可以处理一次像 pragma 这样的包含守卫。即他们不打开文件并避免文件 IO 损失。

        在不支持 pragma 的编译器中,我看到手动实现有点麻烦..

        #ifdef FOO_H
        #include "foo.h"
        #endif
        

        我个人喜欢 #pragma once 方法,因为它避免了命名冲突和潜在的拼写错误的麻烦。相比之下,它也是更优雅的代码。也就是说,对于可移植代码,除非编译器抱怨,否则两者兼有应该没有什么坏处。

        【讨论】:

        • “也就是说,现在的编译器(包括 GCC)足够聪明,可以像 pragma 一样处理包含守卫。” 他们已经这样做了几十年,可能比 @987654323 更长@已经存在!
        • 认为你误解了我的意思。我的意思是在 pragma once 之前说,所有编译器都会为在预处理器阶段多次包含的同一个 h 文件产生多个 IO 惩罚。现代实现最终在预处理器阶段使用更好的文件缓存。无论如何,如果没有编译指示,预处理器阶段最终仍会包含包含守卫之外的所有内容。使用pragma once,整个文件都被遗漏了。从这个角度来看,pragma 仍然是有利的。
        • 不,这是错误的,即使没有#pragma 一次,体面的编译器也会将整个文件排除在外,他​​们不会再次打开文件,甚至不会再查看它,请参阅gcc.gnu.org/onlinedocs/cpp/Once-Only-Headers.html(这与缓存无关)。
        • 从您的链接看来,优化似乎只发生在 cpp 中。无论如何,缓存确实发挥了作用。预处理器如何知道包含守卫之外的代码。示例... extern int foo; #ifndef INC_GUARD #define INC_GUARD 类 ClassInHeader{}; #endif 在这种情况下,预处理器必须包含 extern int foo;如果您多次包含同一个文件,则多次。一天结束,只要我们了解#pragma once 和 include 守卫之间的区别以及各种编译器如何处理它们 :)
        • 显然没有应用优化。
        【解决方案13】:

        如果我们使用 msvc 或 Qt(最高 Qt 4.5),由于 GCC(最高 3.4),msvc 都支持#pragma once,我看不出没有理由不使用#pragma once

        源文件名通常与类名相同,而且我们知道,有时我们需要重构,重命名类名,然后我们也不得不更改#include XXXX,所以我认为手动维护#include xxxxx 不是一个聪明的工作。即使使用 Visual Assist X 扩展,维护“xxxx”也不是必须的工作。

        【讨论】:

          【解决方案14】:

          对那些认为总是需要自动一次性包含头文件的人的补充说明:几十年来,我使用双重或多重包含头文件来构建代码生成器。特别是对于协议库存根的生成,我发现拥有一个非常便携且功能强大的代码生成器非常舒服,无需额外的工具和语言。我不是唯一一个使用这个方案的开发者this blogs X-Macros show。如果没有缺少的自动防护,这将是不可能的。

          【讨论】:

          • C++ 模板能解决这个问题吗?由于 C++ 模板的方式,我很少发现对宏的任何有效需求。
          • 我们长期的专业经验是,始终使用成熟的语言、软件和工具基础架构为我们作为服务提供商(嵌入式系统)提供了生产力和灵活性方面的巨大优势。相反,开发基于 C++ 的嵌入式系统软件和堆栈的竞争对手可能会发现他们的一些开发人员在工作中更快乐。但我们通常在上市时间、功能和灵活性方面多次超越他们。 Nether 低估了一遍又一遍地使用同一个工具所带来的生产力收益。相反,Web-Devs 确实受到许多框架的影响。
          • 一个注意事项:没有在每个头文件中包含一次 guards/#pragma 以违反 DRY 原则本身。我可以在 X-Macro 功能中看到您的观点,但这不是包含的主要用途,如果我们坚持使用 DRY,它不应该像 header unguard/#pragma multi 那样反过来吗?
          • DRY 代表“不要重复自己”。是关于人的。机器在做什么,与那个范式无关。 C++ 模板重复很多,C 编译器也这样做(例如循环展开),每台计算机都在重复几乎所有令人难以置信的事情,而且速度很快。
          • 非常好@Marcel。 X-Macro 概念确实非常有用,并且要创建一个用于该方案的头文件,需要允许头文件被多次包含。与往常一样,整体理解很重要。我倾向于区分写为 X-Macro 的标头和其他标头,即打算包含一次的经典标头。可能使用的命名约定允许生成这些文件的沸腾板代码的编辑器工具来区分文件的用途,以及代码阅读器。
          【解决方案15】:

          我使用 #ifndef/#define 包含使用包含 UUID 的符号的守卫,如下所示:

          #ifndef ARRAY__H_81945CB3_AEBB_471F_AC97_AB6C8B220314
          #define ARRAY__H_81945CB3_AEBB_471F_AC97_AB6C8B220314 /* include guard */
          
          
          #endif
          

          我一直使用能够自动生成 UUID 的编辑器。 这可以防止名称与其他库中具有相同基本名称的文件发生名称冲突,并检测完全相同的文件是否放置在文件系统内的多个位置。

          不利的一面是增加了表格的大小,因为符号要大得多,但我还没有看到它有问题。

          【讨论】:

          • 还可以在名称中加入其他元素,例如图书馆名称、组织名称、创建日期。这些将自动生成,并会降低已经非常小的发生冲突的可能性。
          • 如果有人想了解如何在 Emacs 中完成此操作,请查看我在 GitHub 上的 PEL 项目,特别是 PEL manual section on C headers 以及 pel-uuid.elpel-skels-c.el 文件.
          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 2010-11-11
          • 2016-09-17
          • 2012-01-19
          • 1970-01-01
          • 2019-04-25
          相关资源
          最近更新 更多