#pragma曾经vs包括守卫?

Mat*_*ice 343 c++ coding-style

我正在研究一个已知只能在Windows上运行并在Visual Studio下编译的代码库(它与excel紧密集成,所以它不会去任何地方).我想知道我是否应该使用传统的包含警卫或#pragma once用于我们的代码.我认为让编译器处理#pragma once会产生更快的编译,并且在复制和粘贴时不易出错.它也稍微不那么难看;)

注意:为了获得更快的编译时间,我们可以使用Redundant Include Guards,但这会在包含的文件和包含文件之间增加紧密耦合.通常它没关系,因为防护应该基于文件名,并且只有在你需要改变包含名称时才会改变.

Bri*_*ndy 298

我不认为它会在编译时间上产生显着差异,但是#pragma once在编译器中得到了很好的支持,但实际上并不是标准的一部分.预处理器可能会更快一些,因为它更容易理解您的确切意图.

#pragma once 不太容易犯错误,输入的代码也少.

为了加快编译时间,只需向前声明而不是包含在.h文件中.

我更喜欢使用#pragma once.

请参阅此维基百科文章,了解使用两者的可能性.

  • 我不明白`#pragma once`所谓的可靠性问题.如果我有两个不同的头文件`foo/string.h`和`bar/string.h`,那么放一个`#pragma once`就意味着我可以将它们包括在内,但包含它们两个也没关系.如果我使用包含保护,那么我可能会在两个文件中写一些类似`#ifndef STRING_H`的东西 - 这意味着我不能同时包含foo/string.h和bar/string.h. (42认同)
  • 根据维基百科,一些编译器优化了pragma一次,有些(如gcc)也优化包括守卫.我的直觉说用你的代码已经使用的任何系统.我一次使用两个pragma和守卫. (30认同)
  • `#pragma once`不可靠(不同的文件层次结构透视图(当前工作目录),软链接和硬链接,网络文件系统,甚至名称冲突 - 使用名为`string.h`的文件或类似的文件 - - ).没有考虑速度,你可以有一个脚本替换任何文件中的`%INCLUDE_GUARD%`,用于自动管理的保护符号; 你会把标题写成`header.hpp.in`,并且,因为你已经有了预处理器条件对,所以最终标题不会流动,编译器会在诊断中发出正确的行号. (11认同)
  • @Donnie - 这是gcc的预处理器适用于包含警卫的优化.它会自动检测头文件中的包含保护,如果有,它会将文件视为#pragma一次.如果它再次被包含,并且包含保护在同一时间内未被定义,那么该文件不会被重新打开或重新解析. (10认同)
  • @Brandin如果您有两个名为`string.h`的文件,如果您还没有使用名称空间,则可能最终会发生内部名称冲突,因为具有该名称的两个文件都可能执行涉及字符串的操作,如果您使用名称空间,我认为您应该在include guard标识符中包含名称空间. (10认同)
  • 它可以在编译时间上产生显着差异,因为不必涉及C预处理器 - 编译器本身可以决定不打开文件.一遍又一遍地打开文件需要循环. (5认同)
  • @Brandin但这完全在你的控制范围内(阅读:只是不要这样做;你为什么要这样做?)。使用“#pragma Once”(有详细记录)遇到可靠性问题可能很大程度上“超出”您的控制范围。 (2认同)

Coo*_*kie 168

我只是想在这个讨论中加入我只是在VS和GCC上编译,并且习惯使用包含警戒.我现在已切换到#pragma once,我唯一的原因不是性能或可移植性或标准,因为只要VS和GCC支持它,我就不在乎标准是什么,那就是:

#pragma once 减少错误的可能性.

将头文件复制并粘贴到另一个头文件,修改它以满足需要,并忘记更改包含保护的名称是非常容易的.一旦包含两者,您需要一段时间来追踪错误,因为错误消息不一定清楚.

  • 这是正确的原因。忘记性能——我们应该使用`#pragma once`,因为它不太容易出错。我突然想到,如果编译器已经在跟踪包含守卫,那么当他们看到使用相同宏名称的不同文件时,他们已经完成了发出警告所需的大部分工作。 (4认同)
  • 这正是发生在我身上的事情,这让我非常沮丧。花了几个小时才弄明白。 (2认同)

zwo*_*wol 109

#pragma once无法修复的错误.永远不应该使用它.

如果你的#include搜索路径足够复杂,编译器可能无法告诉两个头之间的区别与相同基(如a/foo.hb/foo.h),所以#pragma once他们在一个将抑制两种.它也可能无法分辨两个不同的相对包含(例如,#include "foo.h"并且#include "../a/foo.h"引用相同的文件,因此#pragma once将无法抑制冗余包含它应该具有的时间.

这也会影响编译器避免使用#ifndef警卫重新读取文件的能力,但这只是一种优化.使用#ifndef警卫,编译器可以安全地读取它不确定已经看过的任何文件; 如果它错了,它只需要做一些额外的工作.只要没有两个头定义相同的保护宏,代码就会按预期编译.如果两个标头确实定义了相同的保护宏,程序员可以进入并更改其中一个.

#pragma once没有这样的安全网 - 如果编译器对头文件的身份是错误的,无论哪种方式,程序都将无法编译.如果您遇到此错误,您唯一的选择是停止使用#pragma once或重命名其中一个标题.标头名称是API合同的一部分,因此重命名可能不是一种选择.

(为什么这是不可修复的简短版本是Unix和Windows文件系统API都没有提供任何保证告诉你两个绝对路径名是否引用同一文件的机制.如果你认为inode号可以用于对不起,你错了.)

(历史记录:大约12年前,当我有权这样做时,我没有撕裂#pragma once#import退出海湾合作委员会的唯一原因是苹果公司的系统标题依赖于它们.回想起来,这不应该阻止我.)

(因为评论主题现在已经出现了两次:GCC开发人员确实付出了相当大的努力以使其#pragma once尽可能可靠;请参阅GCC错误报告11569.但是,当前版本的GCC中的实现仍然可能失败条件,例如构建农场遭受时钟偏差.我不知道其他编译器的实现是什么样的,但我不希望任何人做得更好.)

  • 仅供参考[正在讨论Reddit](https://www.reddit.com/r/cpp/comments/4cjjwe/come_on_guys_put_pragma_once_in_the_standard/d1j04te).人们不相信你的论点(我认为他们有一点意见). (48认同)
  • 在技​​术层面上我同意你的意见,但是如果这两个包括警卫具有相同的名称,因为文件具有相同的名称(这很可能),没有什么赢得.我错了吗? (31认同)
  • @zwol没关系,如果你这样做,那么它不应该有`#pragma once` :)同样,必须为该pragma定义某种理智的语义.如果你想多次包含同一个东西,不要把`#pragma once`放在里面:) (19认同)
  • @zwol"除非两次包含相同的文件正是程序员想要的,否则不会这样做会导致编译失败." 这对我来说似乎不是问题,因为除了罕见的边缘情况之外,这样的文件不会有包含警卫.这种情况可以通过不对该文件使用`#pragma once`来解决. (16认同)
  • @zwol我很抱歉,但如果A/foo.h和B/foo.h是相同的**并且**包括警卫,那么必要时包括警卫也是相同的,第二次包含仍然会被忽略无论如何什么宏定义有效,除非这些定义以某种方式改变了包含守卫的含义(他们可以吗?他们做什么?).对于预处理器"计算"目的而言,多次包含的Boost源不包括防护IIRC,它们对于它们来说是没有意义的.所以,除非我不了解你,否则我认为这不是一个真正的问题. (16认同)
  • @Bim"如果两个标题确实定义了相同的保护宏,程序员可以进入并更改其中一个." (10认同)
  • "如果您的#include搜索路径足够复杂,编译器可能无法区分具有相同基本名称的两个头文件(例如/ foo.h和b/foo.h)之间的区别,所以#pragma就是其中一个他们会压制两者."你能为此提供一个来源吗? (9认同)
  • @zwol我在2003年发现了这个错误报告:https://gcc.gnu.org/bugzilla/show_bug.cgi?id = 11569; 它最后说:"#pragma曾经在3.4中被废弃,因为它[gcc]最后包含了一个正确的实现." 所以该死的东西大约13年以来完美无缺,但FUD还在传播吗? (9认同)
  • 如果文件具有相同的内容,可能它们是同一个文件?我认为这应该足够好,不需要乱用inode号码或其他黑客...... (7认同)
  • @ Jean-MichaëlCelerier那是指https://gcc.gnu.org/ml/gcc-patches/2003-07/msg02780.html,其中Neil Booth*认为他已经成功地正确地实现了该死的东西,但是,阅读代码和后续的线程,他没有*解决我发布这个时所考虑的所有角落情况.在网络文件系统存在的情况下,最可能的失败情况是假阴性(文件包含两次,当它不应该存在),其最后修改时间戳是自我不一致的. (7认同)
  • @JordanMelo换句话说:如果你是编译器程序员,你就不会手动远离"罕见的边缘情况". (6认同)
  • @KubaOber您错过了A/foo.h和B/foo.h可能是_textually_相同的可能性,但是包含(作为`#include"foo.h"`),由A/bar.h和B/bar组成. h分别,_with具有不同的宏定义.您可能会认为这太令人担心,但正如我之前所说,编译器作者不会因为过于担心而无法解雇任何事情. (6认同)
  • @KubaOber请接受假设有一个头文件`foo.h`**应该只*一次,这取决于包含它的文件定义的宏.想象一下,这个头文件被复制到两个库A和B中,它们以不同的方式使用它,然后将这两个库复制到一个更大的程序中.在#ifndef守卫或你提议的#pragma一次性语义下,这将会破坏.**但是对于#ifndef警卫,有一个明显的修复,维护较大程序的人可以应用而不影响其他任何事情:**更改保护宏. (6认同)
  • 我从来没有遇到#pragma一次的问题.我只有#ifndef的问题.通常当我重命名文件然后用自动生成的#ifndef创建new.已经存在,因为我确实重命名了文件,而不是安全卫士.好的#pragma曾经有bug,我认为这真的很少见.它可以通过修改文件来解决.就像#ifndef的问题一样.我仍然看不出为什么不使用它们.没有什么是100%完美的. (5认同)
  • @zwol如果在编译期间重新安装文件系统,则行为未定义,如果在编译期间更改任何文件,则行为未定义.这不是一个真正的问题. (5认同)
  • @KonradRudolph我在想 - 为什么它突然吸引了更多的选票.我没有时间试图说服人们比我已经拥有的更多; 如果他们不相信,哦,好吧. (4认同)
  • @JordanMelo我觉得我们在这里"编译器无法分辨你的意思".不要以为你正在尝试修复一个应用程序,它突破了我概述的错误.相反,想象一下你正试图*实现*`#pragma once`.如果只提供编译器可用的信息,特别是*不*应用程序员心中的内容,你如何让它在面对*任意的,未知的,不可预测的*包括路径和宏hackery时合理地表现? (4认同)
  • @zwol`如果你不认为最后修改时间戳是自我不一致的网络文件系统是值得担心的情况,我羡慕你必须领导的生活.我在工作中遇到这个 - 每周的一天,每次我编译_.这不是一个极端的案例.如果有任何问题,那么在使用网络文件系统时,最后修改的时间戳将是一致的.话虽如此,我总是使用`#pragma once`,并且从未遇到问题.我知道它无法可靠地实施,但如果你不做蠢事,你就可以了. (4认同)
  • 如果你有一个相同的包含文件,那就多次包含它是没有意义的,**iff**里面有一个`#pragma once`.这种行为应该恕我直言标准化.我认为使用一对(文件大小,加密摘要)的实现将"足够接近".那么文件的来源并不重要:它可以通过管道,标准输入,网络套接字等提供.如果预处理器将其作为"文件"读取,并且它具有编译指示,并且它与先前读取的另一个"文件"相同,则会被忽略.十分简单. (4认同)
  • @KubaOber相比之下,对于你提出的#pragma一次性语义,还有一个修复 - 更改注释中的任何文本,比如说 - 但是从代码检查来看并不明显,维护者将不得不查找它手册,然后担心这是否适用于所有支持的编译器.因此,#ifndef guards在用户体验方面是更好的语言功能. (4认同)
  • 任何基于文件名的解决方案本身都是无用的浪费时间,不需要进一步讨论任何基于文件名或路径的解决方案.现在,文件的校验和不仅在具有时钟偏差的网络驱动器上保持一致,而且在具有完全不同名称和路径的重复项之间甚至是一致的.这正确地实现了`#pragma once`的唯一合理使用,即使用冗余包含树组织数据结构定义.将此称为"不可修复的错误"是有偏见的或天真的,并且"任意不可预测的"路径路障是一个红色的鲱鱼. (4认同)
  • @static_rtti请记住,一般而言,C声明是_not_幂等的。两次包含相同的文件,或者包含两个具有相同内容的文件,通常会导致编译失败。除非程序员两次想要包含同一文件,否则正是程序员想要的,否则将导致编译失败。弄清楚您所处的情况可能等同于停止问题。 (3认同)
  • 你描述了pragma曾经失败的情况,但#ifndef也是如此.那么重点是什么呢?您必须更改文件的内容.如果你在任何地方使用prama,你可以将#ifndef添加到你的文件中,就是这样.如果您在任何地方都使用#ifndef,那么您可以更改它.如果您对某些文件有特殊需要,可以使用#ifndef,使用pragma一次就不会禁用这种可能性. (3认同)
  • @fbucek他的观点是"你需要更改文件的内容"对于普通的包含保护来说更明显,并且取决于特定编译器如何实现它,可能不足以保证`#pragma once`的成功.基本上,对于包括警卫,你可以一眼就看出你需要换一个守卫; 但是,使用`#pragma once`,您需要查看编译器的文档,看看是否需要更改文件的内容,更改其名称或内容. (3认同)
  • 那个前提是一个谬论!好吧,"不理智"是一种观点,所以你绝对*做*声明任何人的代码库是否理智."一次"编译指示并非严格标准化,因此从技术上讲,任意选择都是有效的.正如你上面所说的那样:你可能在几年前就把它撕掉了,肯定会破坏*多个代码库,而不仅仅是切换到校验和.这个问题明确地要求减少冗余包含,这是编程中常见且有用的任务,而不是模糊滥用"曾经",正如你所说的,应该在很久以前就已经被破坏和重写了. (3认同)
  • “只要没有两个标头定义相同的保护宏......”是一个无法修复的错误。 (3认同)
  • @ Jean-MichaëlCelerier(如果你不认为最后修改时间戳是自我不一致的网络文件系统是值得担心的情况,我羡慕你必须领导的生活.) (2认同)
  • @KubaOber(我什至不认为这牵强。我怀疑您使用的Boost预处理程序库的某些部分完全一样。) (2认同)
  • @KubaOber 我只是要不断重复“编译器作者不会因为太牵强而无需担心而忽略任何事情”,直到完全理解为止 (2认同)
  • @zwol所以,解决方法始终是修改文件,除了包含警卫使其更明显? (2认同)
  • @KubaOber 对。但请注意,这仅适用于 #pragma once 的 _your_ 语义。更面向路径名的语义可能需要您在发生冲突时重命名文件,这可能会破坏 API 合同。编译器历来并不擅长记录他们如何实现这些东西,所以你怎么知道该怎么做?我正在考虑如何修改我的答案以更好地表达这些事情。 (2认同)
  • 我认为没有办法让路径名以易于指定的方式工作,并且可以在随机平台上实现.如果要标准化"#pragma once"的行为,则甚至不应提及术语"路径".包括守卫不使用路径,除了人类通常在那里设置一条受损的路径,并祈祷它不会与任何东西发生冲突. (2认同)
  • GCC修复了文件名方法,并在发布此"答案"之前13年处理了正确的符号链接.即使没有文件名或inode,它仍然可以通过使用校验和来解决.编辑:https://gcc.gnu.org/bugzilla/show_bug.cgi?id = 11569 (2认同)
  • 为什么不能使用inode号?inode和设备编号的组合对于系统上的每个文件都是唯一的. (2认同)
  • zwol 我阅读了您链接的整个主题,没有发现任何问题?所有看到的问题最终都得到了解决。 (2认同)

Kla*_*aim 35

直到这一天#pragma once成为标准(目前不是未来标准的优先级),我建议你使用它并使用警卫,这样:

#ifndef BLAH_H
#define BLAH_H
#pragma once

// ...

#endif
Run Code Online (Sandbox Code Playgroud)

原因是:

  • #pragma once不是标准的,因此某些编译器可能不提供该功能.也就是说,所有主流编译器都支持它.如果编译器不知道它,至少它将被忽略.
  • 由于没有标准行为#pragma once,您不应该假设所有编译器的行为都是相同的.警卫将至少确保所有编译器的基本假设是相同的,至少为警卫实施所需的预处理器指令.
  • 在大多数编译器中,#pragma once将加速编译(一个cpp),因为编译器不会重新打开包含该指令的文件.因此,将其置于文件中可能有所帮助,具体取决于编译器.我听说g ++可以在检测到防护时进行相同的优化,但必须进行确认.

将两者结合使用,您可以获得每个编译器的最佳效果.

现在,如果你没有一些自动脚本来生成警卫,那么使用它可能会更方便#pragma once.只知道这对便携式代码意味着什么.(我正在使用VAssistX快速生成警卫和编译指示)

您应该几乎总是以可移植的方式思考您的代码(因为您不知道将来会做什么)但是如果您真的认为它不是要用其他编译器编译(例如,非常特定的嵌入式硬件的代码)那么你应该检查你的编译器文档#pragma once,了解你真正在做什么.

  • 谁说#pragma曾经是标准的?此外,有多少编译器不跟踪头文件是否完全包含在包含保护中?换句话说 - 有人测量过#pragma曾经真正在现实中产生影响吗? (6认同)
  • @DarioP:识别它的编译器不关心.当包含保护之外的东西时,不识别它的编译器有被禁用的风险包括保护优化.没有优化的编译器会在重复包含时跳过防护内部的所有内容,因此内部处理涉及的处理更少. (6认同)
  • @Richard,我有,而且确实如此 - 我们的项目有5500包括大约1/2冗余. (4认同)
  • 此处的“ #pragma一次”将永远不会被正确评估,因此是多余的,因为后卫将始终覆盖它。不用担心,只需在较简单的项目中使用老式的防护罩或仅使用编译指示即可。 (3认同)
  • @ 280Z28:显然,5500的完全重建包括50%的冗余会花费一些成本.但是,你能提供数字,即.使用编译时一次编译时间(如果你愿意,请使用干净)与不使用它.如果你达到完全清洁构造的总补偿时间的1%以上,我会感到震惊. (2认同)
  • 在包含守卫之后拥有`#pragma once`*的原因是什么? (2认同)
  • 微软似乎已经优化了他们的编译器。来自Microsoft的VS2015文档:“在同一文件中同时使用#include保护语和#pragma没有优势。编译器识别#include保护语并以与#pragma相同的方式实现多重include优化。如果没有成语的标准形式之前或之后没有非注释代码或预处理器指令,则该指令” https://msdn.microsoft.com/zh-cn/library/4141z1cx.aspx (2认同)

Kon*_*ine 32

从软件测试人员的角度来看

#pragma once 比大多数编译器支持的包含保护更短,容易出错,并且有些人说它编译速度更快(不再那么真实).

但是我仍然建议你选择标准#ifndef包含警卫.

为什么#ifndef

考虑这样一个人为的类层次结构,其中每个班的A,BC住自己的文件中:

#ifndef A_H
#define A_H

class A {
public:
  // some virtual functions
};

#endif
Run Code Online (Sandbox Code Playgroud)

BH

#ifndef B_H
#define B_H

#include "a.h"

class B : public A {
public:
  // some functions
};

#endif
Run Code Online (Sandbox Code Playgroud)

CH

#ifndef C_H
#define C_H

#include "b.h"

class C : public B {
public:
  // some functions
};

#endif
Run Code Online (Sandbox Code Playgroud)

现在让我们假设您正在为您的类编写测试,并且您需要模拟真正复杂类的行为B.一种方法是使用例如google mock编写一个mock类并将其放在一个目录中.请注意,类名称没有更改,但它只存储在不同的目录中.但最重要的是包含警卫的名称与原始文件中的名称完全相同.mocks/b.hb.h

嘲笑/ BH

#ifndef B_H
#define B_H

#include "a.h"
#include "gmock/gmock.h"

class B : public A {
public:
  // some mocks functions
  MOCK_METHOD0(SomeMethod, void());
};

#endif
Run Code Online (Sandbox Code Playgroud)

有什么好处?

使用这种方法,您可以模拟类的行为,B而无需触及原始类或告诉C它.您所要做的就是将目录mocks/放在编译器的包含路径中.

为什么不能这样做#pragma once呢?

如果你曾经使用过#pragma once,你会得到一个名字冲突,因为它不能保护你不要B两次定义类,一次是原始类,一次是模拟版.

  • @parapurarajkumar ODR没有被违反,因为如果你包括mocks/bh之前bh预处理器将跳过bh完全让你只有一个B类.我想知道你是否有一个不那么"痛苦"的方法来测试B类.你采取什么策略?建议测试"任何大型项目"? (4认同)
  • 这对任何大型项目都不实用.保持B up2date会很痛苦,更不用说这违反了One Definition Rule (2认同)
  • 没人压倒"gmock.h".它是我们想要模拟一个类的Testframework的一部分.我想我们谈论的是两件不同的事情.重点是采用现有代码或类层次结构,并作为测试用模拟器或模拟类替换链的一部分.这样您就可以有效地测试非测试代码.它不像单元测试,你只测试一个类,但是模拟允许你做的是系统测试.如果你的类层次结构使用经典的#ifndef include guard,你可以在测试中用mock替换一个类. (2认同)
  • 嗯,如果原始的`bh`和模拟`bh`都在编译器中包含路径 - 那么就不会出现与`#include"bh"`的名字冲突?如果从测试项目的include目录中删除原始的`bh`路径,以便只拾取模拟`bh`,那么`#pragma once`也不会起作用? (2认同)
  • IMO如果您要在包含路径中添加“ mocks / bh”,则还应删除原始的“ bh”,而不要依赖包含防护。您的解决方案就像符号插入一样la脚:-/ (2认同)

Don*_*oer 23

如果你是积极的,你永远不会在不支持它(在Windows/VS,GCC,以及锵是编译器的例子编译器使用此代码支持它),那么你当然可以使用#pragma一次无后顾之忧.

您也可以同时使用它们(参见下面的示例),以便在兼容系统上获得可移植性和编译加速

#pragma once
#ifndef _HEADER_H_
#define _HEADER_H_

...

#endif
Run Code Online (Sandbox Code Playgroud)

  • #pragma曾经在许多不同的编译器(包括GCC)上得到很好的支持 (26认同)
  • 是的,但并不是普遍支持的.始终支持包括警卫......取决于您希望代码的可移植性. (6认同)
  • 我真的不想要两者,只会让代码难看.只要便携性不是问题,我认为代码卫生问题是下一个问题. (6认同)
  • 即使您关心它,便携性也不是什么大不了的事.如果您目前正在编写仅适用于Windows的代码,那么代码中可能还有其他一些东西会比#pragma一次更紧密地绑定Windows.如果有一天你应该填写一些代码,那么编写一个遍历头文件的Perl脚本并用基于文件名和路径的include guard代替#pragma的所有用法并不困难. (6认同)
  • 另一方面,如果项目的一部分正在编写最终可能在其他地方使用的位(例如数学库,api包装器等),那么就可以进行移植.你担心你的代码应该如何看待,而不是关于它应该如何工作,你会担心...尤其是在人们已经学会忽视的文件的顶部. (3认同)

flu*_*ffy 19

在进行了关于假设#pragma once#ifndef保护之间的假设性能权衡与正确与否的论证的延伸讨论之后(我#pragma once基于最近的一些灌输为基础),我决定最终测试#pragma once更快的理论因为编译器不必尝试重新#include创建已包含的文件.

为了测试,我自动生成了500个具有复杂相互依赖性的头文件,并且有一个.c文件#include全部都是.我用三种方式进行了测试,一次只用#ifndef一次#pragma once,一次用两次,一次用两种方式.我在一个相当现代的系统上进行了测试(运行OSX的2014 MacBook Pro,使用XCode捆绑的Clang,内置SSD).

一,测试代码:

#include <stdio.h>

//#define IFNDEF_GUARD
//#define PRAGMA_ONCE

int main(void)
{
    int i, j;
    FILE* fp;

    for (i = 0; i < 500; i++) {
        char fname[100];

        snprintf(fname, 100, "include%d.h", i);
        fp = fopen(fname, "w");

#ifdef IFNDEF_GUARD
        fprintf(fp, "#ifndef _INCLUDE%d_H\n#define _INCLUDE%d_H\n", i, i);
#endif
#ifdef PRAGMA_ONCE
        fprintf(fp, "#pragma once\n");
#endif


        for (j = 0; j < i; j++) {
            fprintf(fp, "#include \"include%d.h\"\n", j);
        }

        fprintf(fp, "int foo%d(void) { return %d; }\n", i, i);

#ifdef IFNDEF_GUARD
        fprintf(fp, "#endif\n");
#endif

        fclose(fp);
    }

    fp = fopen("main.c", "w");
    for (int i = 0; i < 100; i++) {
        fprintf(fp, "#include \"include%d.h\"\n", i);
    }
    fprintf(fp, "int main(void){int n;");
    for (int i = 0; i < 100; i++) {
        fprintf(fp, "n += foo%d();\n", i);
    }
    fprintf(fp, "return n;}");
    fclose(fp);
    return 0;
}
Run Code Online (Sandbox Code Playgroud)

现在,我的各种测试运行:

folio[~/Desktop/pragma] fluffy$ gcc pragma.c -DIFNDEF_GUARD
folio[~/Desktop/pragma] fluffy$ ./a.out 
folio[~/Desktop/pragma] fluffy$ time gcc -E main.c  > /dev/null

real    0m0.164s
user    0m0.105s
sys 0m0.041s
folio[~/Desktop/pragma] fluffy$ time gcc -E main.c  > /dev/null

real    0m0.140s
user    0m0.097s
sys 0m0.018s
folio[~/Desktop/pragma] fluffy$ time gcc -E main.c  > /dev/null

real    0m0.193s
user    0m0.143s
sys 0m0.024s
folio[~/Desktop/pragma] fluffy$ gcc pragma.c -DPRAGMA_ONCE
folio[~/Desktop/pragma] fluffy$ ./a.out 
folio[~/Desktop/pragma] fluffy$ time gcc -E main.c  > /dev/null

real    0m0.153s
user    0m0.101s
sys 0m0.031s
folio[~/Desktop/pragma] fluffy$ time gcc -E main.c  > /dev/null

real    0m0.170s
user    0m0.109s
sys 0m0.033s
folio[~/Desktop/pragma] fluffy$ time gcc -E main.c  > /dev/null

real    0m0.155s
user    0m0.105s
sys 0m0.027s
folio[~/Desktop/pragma] fluffy$ gcc pragma.c -DPRAGMA_ONCE -DIFNDEF_GUARD
folio[~/Desktop/pragma] fluffy$ ./a.out 
folio[~/Desktop/pragma] fluffy$ time gcc -E main.c  > /dev/null

real    0m0.153s
user    0m0.101s
sys 0m0.027s
folio[~/Desktop/pragma] fluffy$ time gcc -E main.c  > /dev/null

real    0m0.181s
user    0m0.133s
sys 0m0.020s
folio[~/Desktop/pragma] fluffy$ time gcc -E main.c  > /dev/null

real    0m0.167s
user    0m0.119s
sys 0m0.021s
folio[~/Desktop/pragma] fluffy$ gcc --version
Configured with: --prefix=/Applications/Xcode.app/Contents/Developer/usr --with-gxx-include-dir=/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.12.sdk/usr/include/c++/4.2.1
Apple LLVM version 8.1.0 (clang-802.0.42)
Target: x86_64-apple-darwin17.0.0
Thread model: posix
InstalledDir: /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin
Run Code Online (Sandbox Code Playgroud)

正如您所看到的那样,#pragma once预处理的版本确实比#ifndef仅使用它的版本略快一些,差异可以忽略不计,并且会远远超过实际构建和链接代码所花费的时间.也许有足够大的代码库可能会导致几秒钟的构建时间不同,但现代编译器之间能够优化#ifndef防护,操作系统具有良好的磁盘缓存,以及存储技术的速度越来越快,似乎性能论证没有实际意义,至少在这个时代的典型开发者系统上是这样.较旧且更具异国情调的构建环境(例如,托管在网络共享上的标头,从磁带构建等)可能会稍微改变等式,但在这些情况下,首先简单地构建不太脆弱的构建环境似乎更有用.

问题的事实是,#ifndef标准行为是标准化的,而#pragma once不是,并且#ifndef还处理奇怪的文件系统和搜索路径#pragma once极端情况,但是可能会被某些事情弄得很困惑,导致程序员无法控制的错误行为.主要的问题#ifndef是程序员为他们的警卫选择坏名称(名称冲突等),即使这样,API的消费者也很可能使用#undef- 不是一个完美的解决方案来覆盖那些可怜的名字,但这是可能的,#pragma once如果编译器错误地剔除了一个,则没有追索权#include.

因此,即使 #pragma once明显(稍微)更快,我也不同意这本身就是将其用于#ifndef警卫的理由.

编辑:感谢来自@LightnessRacesInOrbit的反馈,我增加了头文件的数量,并将测试更改为仅运行预处理器步骤,消除了编译和链接过程中添加的任何少量时间(这在以前是微不足道的现在不存在).正如所料,差异大致相同.

  • 您的答案提供的背景不足以判断随着项目的增长,节省的费用是否会保持0.04秒不变,或者随着项目的增长,或者介于两者之间的某个位置,将保持不变.因此,它不是一个非常有用的基准,目前不支持"微不足道"的结论.我只是提到它,因为我对真正的结论非常感兴趣! (5认同)
  • 这是一个很好的观点 - 它只测量包含防护的速度,而不测量预处理器的其余部分。我认为很明显,生成的代码只是测试该方面,因为防护线之间没有太多东西。也许我可以扩展生成的代码,使每个文件都有几千个随机生成的模板函数和类,这会导致不同的不切实际的综合测试。 (2认同)

Mic*_*urr 15

我通常不打扰,#pragma once因为我的代码有时必须用MSVC或GCC以外的东西进行编译(嵌入式系统的编译器并不总是有#pragma).

所以我不得不使用#include guards.我也可以使用#pragma once一些答案建议,但似乎没有太多理由,它往往会导致不支持它的编译器发出不必要的警告.

我不确定pragma可能会节省多少时间.我听说编译器通常已经识别出标题什么时候除了保护宏之外什么都没有,并且#pragma once在这种情况下会做同等的事情(即,永远不再处理文件).但我不确定它是否属实,或者仅仅是编译器可以进行优化的情况.

无论哪种情况,我都可以更轻松地使用#include警卫,它可以在任何地方使用,而不用担心它.

  • 我很好奇哪个编译器不支持它.大多数编译器都会这样做,而我不知道哪些编译器没有. (6认同)

Mot*_*tti 10

这里有一个相关的问题我回答:

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

我也在这里添加答案,以防有人绊倒这个问题,而不是另一个.

  • 修复`#pragma once`实现,它应该使用可靠的校验和而不是粗略的文件名. (5认同)

Ric*_*den 9

我认为你应该做的第一件事是检查这是否真的会产生影响,即.你应该首先测试性能.其中一个谷歌搜索引发了这一点.

在结果页面中,对于我来说,这些列是关闭的,但很明显,至少VC6 microsoft没有实现其他工具正在使用的包含保护优化.如果包括后卫是内部的,那么包括后卫在外部的时间要长50倍(外部包括后卫至少和#pragma一样好).但是让我们考虑一下这可能产生的影响:

根据提供的表格,打开包含和检查它的时间是#pragma等价物的50倍.但实际这样做的时间是在1999年以每张1微秒的速度测量的!

那么,单个TU有多少个重复的标题?这取决于你的风格,但如果我们说平均TU有100个重复,那么在1999年我们可能每个TU支付100微秒.随着HDD的改进,这可能会显着降低,但即使这样,使用预编译的头文件和正确的依赖关系跟踪,项目的总累积成本几乎肯定是构建时间的一个重要部分.

现在,另一方面,尽管可能不太可能,但是如果您转移到不支持的编译器,#pragma once那么请考虑将整个源代码库更新为包含防护而不是#pragma需要多长时间?

没有理由微软无法以与GCC和其他所有编译器相同的方式实现包含保护优化(实际上任何人都可以确认他们的更新版本是否实现了这一点?).恕我直言,#pragma once除了限制你选择的替代编译器之外,做的很少.

  • 不要挑剔,但是如果你移植到一个非pragma-once支持编译器,编写一个关闭工具来拖网文件并用常规包含保护替换#pragma一次可能在整个移植过程中是微不足道的. (7认同)
  • _“考虑将整个源库更新为包括警卫而不是#pragma会花费多少时间?” _不会花很长时间。这么简单就是我写[guardonce](https://github.com/cgmb/guardonce)的原因。 (3认同)
  • _“实际上,任何人都可以确认他们的最新版本是否实现了吗?” _ VS2015文档中提到它现在确实实现了包含防护优化的功能。https://msdn.microsoft.com/zh-CN/library/4141z1cx.aspx (2认同)