我正在研究一个已知只能在Windows上运行并在Visual Studio下编译的代码库(它与excel紧密集成,所以它不会去任何地方)。 我想知道我是否应该使用传统的包含警卫或使用#pragma once代码。 我认为让编译器处理#pragma once会产生更快的编译,并且在复制和粘贴时不易出错。 它也略微不那么;)
注意:为了获得更快的编译时间,我们可以使用Redundant Include Guards,但这会在包含的文件和包含文件之间增加紧密耦合。 通常它是可以的,因为防护应该基于文件名,并且只有在您需要更改包含名称时才会更改。
我不认为它会在编译时产生显着差异,但#pragma once在编译器中得到了很好的支持,但实际上并不是标准的一部分。预处理器可能会更快一些,因为它更容易理解您的确切意图。
#pragma once不容易出错,输入的代码也少。
为了加快编译时间,只需向前声明而不是包含在.h文件中。
我更喜欢使用#pragma once。
请参阅此维基百科文章,了解使用两者的可能性。
-
根据维基百科,一些编译器优化了pragma一次,一些(如gcc)也优化包括守卫。我的直觉说用你的代码已经使用的任何系统。我一次使用两个pragma和守卫。
-
它可以在编译时间上产生显着差异,因为不必涉及C预处理器 - 编译器本身可以决定不打开文件。一遍又一遍地打开文件需要循环。
-
@Donnie - 这是gcc的预处理器适用于包含警卫的优化。它会自动检测头文件中的包含保护,如果有,它会将文件视为#pragma一次。如果它再次被包含,并且包含保护在同一时间内未被定义,那么该文件不会被重新打开或重新解析。
-
没错,但问题的海报是使用VC ++,因此使用#pragma一次可能会导致更快的编译。
-
哦,我明白你的意思了。对不起,我以为你回应了kts的评论。关于"一些编译器(如gcc)"。很明显,你实际上是回应了Brian的第一句话。不理我。
-
我把这一点放在"一些编译器"上,因为我不知道VC ++做了什么(因为我一次使用pragma和守卫)它从来没有重要,因为MS可能决定删除/更改pragma一次行为,因为它不是必需的按标准。为什么他们可能会这样做我不知道,但这不是他们第一次使旧代码与新版本的Visual Studio不兼容。
-
+1也指出包含警卫的容易出错的性质。我已经看到它们在没有经过适当调整的情况下被复制和粘贴 - 我们没有注意到它多年,直到我们试图将两个标题一起包含在一个翻译单元中,并且考虑到它持续了多长时间,这是一个令人头疼的问题。我梦想模块支持,但如果#pragma曾经等同于标准,我仍然会很高兴。
-
关于前向声明(另一件经常没有提到的事情):在减少编译时间的同时,要注意如果你想重构前向声明的类 - 例如更改其名称或命名空间 - 所有这些前向声明都会返回给您,因为您需要更改每个单独的声明。并且它们甚至不会产生明确的编译器错误,因为前向声明仍然有效。所以如果你在许多不同的命名空间中声明了相同的名称,那么按Ctrl Shift F,但是boohoo ...只是注意前向声明也有缺点(我仍然使用它们)
-
#pragma once不可靠(不同的文件层次结构透视图(当前工作目录),软链接和硬链接,网络文件系统,甚至名称冲突 - 与名为string.h的文件或类似的文件 - )。不考虑速度,您可以使用一个脚本替换文件中的%INCLUDE_GUARD%,以获取自动管理的保护符号;您将头文件写为header.hpp.in,并且,因为您已经有预处理器条件对,所以最终头文件不会流动,编译器会在诊断中发出正确的行号。
-
我刚刚学会了pragma once不可靠的困难方法。 (感谢确认,@ Kalrish)Header卫兵确实在pragma once没有的地方工作。 (在MSVC10上)我现在正在使用两者。
-
我曾经从未遇到过pragma的问题,但是,我遇到过包含警卫的问题。我尝试了一个非常基本的测试:1。创建两个文件夹,"my"和"your",2。将文件ifdef.h和pragma.h添加到两个文件夹中,3。使用IFDEF_H保护ifdef,使用pragma保护pragma一旦。 4.在保护区域的每个相应文件中定义MY_IFDEF,YOUR_IFDEF,MY_PRAGMA,YOUR_PRAGMA。 5.将所有内容包含在test.cpp文件中。 6. ifdef测试每个,cout'ing存在或不存在。我发现pragma曾经定义过,但ifdef中的一个定义没有。什么时候pragma失败了?
-
我不明白#pragma once所谓的可靠性问题。如果我有两个不同的标题foo/string.h和bar/string.h,那么放一个#pragma once意味着我可以允许它们包含一次,但包含它们两个也没关系。如果我使用包含保护,那么我可能会在两个文件中写出类似#ifndef STRING_H的东西 - 这意味着我不能同时包含foo / string.h和bar / string.h。
-
@Brandin如果您有两个名为string.h的文件,如果您还没有使用名称空间,则可能最终会出现内部名称冲突,因为具有该名称的两个文件都可能会执行涉及字符串的操作,如果你使用命名空间,在我看来,你应该在include guard标识符中包含命名空间。
-
@Brandin但这完全在你的掌控之中(读:只是不这样做;你为什么要那样做?)。使用#pragma once(已有详细记录)遇到可靠性问题可能在很大程度上超出了您的控制范围。
-
@LightnessRacesinOrbit可能这些名字并不是很好的例子。您可以想象一个包含多个组件的大项目,每个组件都有一个带有常规名称的公共标题,例如"constants.h",所以你最终得到comp1 / constants.h,comp2 / constants.h,....但是每个组件作者都不会真正考虑她的组件将如何与其他组件一起包含,所以可能她会把她的包括后卫写成#ifdef INCLUDED_CONSTANTS_H或类似的东西。例如。标准方式很容易导致包含警卫名称冲突。
-
@Brian R. Bondy也#pragma曾经没有进行健全性检查所以基本上你可以创建一个循环依赖,甚至从来不知道它,以GLM lib为例,type_mat2x2.hp??p包含它的.inl文件,type_mat2x2.inl包括func_matrix.hpp ,func_matrix.hpp包含mat2x2.hp??p,mat2x2.hp??p包含type_mat2x2.hp??p,这是100%的循环依赖,因为最后type_mat2x2.hp??p包含在自身中,但是因为它们使用#pragma once而不是include guard,编译器删除了包含并允许代码正常运行,允许危险的错误
-
@Brandin你不应该在自己的项目中发生名称冲突。正如JAB所指出的,名称空间应该用于消除可重用组件的歧义;或者,您可以使用物理包设计(Java中的要求)来消除歧义。在这两种情况下,系统地在include guard中包含命名空间名称或包路径将消除任何潜在的冲突。请参阅此文章,了解物理设计的实际示例并包含警卫。
-
@Moebius命名空间不适用于预处理程序符号,例如在上面的例子中,即使它们是不同的文件,两个文件也可能具有INCLUDED_CONSTANTS_H或类似于传统包含守卫的文件。当然,您可以通过更充分地命名符号来消除歧义,例如: INCLUDED_COMP1_CONSTANTS_H和INCLUDED_COMP2_CONSTANTS_H,但实际上没有人这样做。
-
我理解布兰丁。我建议如果你有一个名为ABC的包和一个名为DEF的包,无论是否明确使用名称空间声明,你的包含保护可以是INCLUDED_ABC_CONSTANTS和INCLUDED_DEF_CONSTANTS以避免冲突。此外,调用头文件abc_constants.h和def_constants.h可以提高理解力并避免一些可能令人讨厌的签入错误。
我只是想在这个讨论中加入我只是在VS和GCC上编译,并且习惯使用包含警戒。我现在切换到#pragma once,我唯一的原因不是性能或可移植性或标准,因为只要VS和GCC支持它,我就不在乎标准是什么,那就是:
#pragma once减少了错误的可能性。
将头文件复制并粘贴到另一个头文件,修改它以满足需要,并忘记更改包含保护的名称是非常容易的。一旦包含两者,您需要一段时间来追踪错误,因为错误消息不一定清楚。
-
这件事恰巧发生在我身上,令人沮丧。需要几个小时才能搞清楚。
-
这是正确的理由。忘记性能 - 我们应该使用#pragma once,因为它不易出错。在我看来,如果编译器已经跟踪包括警卫,他们已经做了大部分工作,当他们看到使用相同宏名称的不同文件时发出警告。
-
你复制并粘贴,而不考虑你正在复制和粘贴什么?哦!伊克!
#pragma once具有无法修复的错误。永远不应该使用它。
如果您的#include搜索路径足够复杂,编译器可能无法区分具有相同基本名称的两个标头之间的区别(例如a/foo.h和b/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年前,当我有权这样做时,我没有从GCC中删除#pragma once和#import的唯一原因是Apple的系统标题依赖于它们。回想起来,那应该是'我阻止了我。)
(因为在评论主题中现在已经出现了两次:GCC开发人员确实付出了相当大的努力使#pragma once尽可能可靠;请参阅GCC错误报告11569.但是,当前版本的GCC中的实现仍然可以在合理的条件下失败,例如遭受时钟偏差的构建农场。我不知道其他编译器的实现是什么样的,但我不希望任何人做得更好。)
-
在技??术层面上我同意你的意见,但是如果这两个包括警卫具有相同的名称,因为文件具有相同的名称(这很可能),没有什么赢得。我错了吗?
-
@Bim"如果两个标题确实定义了相同的保护宏,程序员可以进入并更改其中一个。"
-
仅供参考,我们正在Reddit上讨论这个答案。人们不相信你的论点(我认为他们有一点意见)。
-
如果文件具有相同的内容,可能它们是同一个文件?我认为这应该足够好,不需要乱用inode号码或其他黑客......
-
@static_rtti请记住,一般来说,C声明不是幂等的。包含两次相同的文件,或两个内容相同的文件,通常会导致编译失败。除非两次包含相同的文件正是程序员想要的并且不这样做会导致编译失败。弄清楚你所处的情况可能与停机问题相同。
-
@KonradRudolph我想知道为什么它突然吸引了更多的选票。我没有时间试图说服人们比我已经拥有的更多;如果他们不相信,哦,好吧。
-
"如果您的#include搜索路径足够复杂,编译器可能无法区分具有相同基本名称的两个头文件(例如/ foo.h和b / foo.h)之间的区别,所以#pragma就是其中一个他们会压制两者。"你能为此提供一个来源吗?
-
@ Jean-MichalCelerier不幸的是,我不能更准确地告诉你,而不是"通过GCC邮件列表和大约2001-2004的bug跟踪器来解决#import和/或#pragma once的问题。"很久以前了。
-
@zwol"除非两次包含相同的文件正是程序员想要的,否则不会这样做会导致编译失败。"这对我来说似乎不是问题,因为除了罕见的边缘情况之外,这样的文件不会有包含警卫。通过不对该文件使用#pragma once可以简单地解决这种情况。
-
@JordanMelo我觉得我们在这里"编译器无法分辨你的意思"。不要以为你正在尝试修复一个应用程序,它绊倒了我概述的错误。相反,想象一下你正在尝试实现#pragma once。如果只提供编译器可用的信息,特别是应用程序员心中的内容,你如何让它在面对任意的,未知的,不可预测的事先包括路径和宏hackery时合理地行为?
-
@JordanMelo换句话说:如果你是编译器程序员,你就不会手动远离"罕见的边缘情况"。
-
@zwol我从2003年发现了这个错误报告:gcc.gnu.org/bugzilla/show_bug.cgi?id = 11569;它最后说:#pragma once has been undeprecated in 3.4 because it [gcc] contains a correct implementation at last.。所以该死的东西大约13年以来完美无缺,但FUD还在传播吗?
-
@ Jean-MichalCelerier指的是gcc.gnu.org/ml/gcc-patches/2003-07/msg02780.html,其中Neil Booth认为他已经成功地正确地实现了该死的东西,但是,阅读代码和随后的线程,他没有解决我发布这个时我想到的所有角落案件。在网络文件系统存在的情况下,最可能的失败情况是假阴性(文件包含两次,当它不应该存在),其最后修改时间戳是自我不一致的。
-
@ Jean-MichalCelerier(如果你不认为最后修改时间戳是自我不一致的网络文件系统是值得担心的情况,我羡慕你必须领导的生活。)
-
@zwol If you don't think a network file system whose last-modification timestamps are self-inconsistent is a scenario worth worrying about, I envy the life you must have lead.我在每周的每一天都遇到过这个问题,每次编译都是这样。这不是一个极端的案例。如果有任何问题,那么在使用网络文件系统时,最后修改的时间戳将是一致的。话虽如此,我总是使用#pragma once,并且从未遇到过它的问题。我知道它不能可靠地实施,但如果你不做蠢事,你就可以了。
-
如果你有一个相同的包含文件,如果它里面有一个#pragma once,就没有必要多次包括它。这种行为应该恕我直言标准化。我认为使用一对(文件大小,加密摘要)的实现将"足够接近"。那么文件的来源并不重要:它可以通过管道,标准输入,网络套接字等提供。如果预处理器将其作为"文件"读取,并且它具有编译指示,并且它与先前读取的另一个"文件"相同,则会被忽略。十分简单。
-
@KubaOber您错过了A / foo.h和B / foo.h可能在文本上相同的可能性,但分别包含(作为#include"foo.h"),分别由A / bar.h和B / bar.h使用不同的宏有效的定义。您可能会认为这太令人担心,但正如我之前所说,编译器作者不会因为过于担心而无法解雇任何事情。
-
@KubaOber(我甚至认为它不是那么牵强,我。我怀疑Boost预处理器库有点像你那样使用。)
-
@zwol没关系,如果你这样做,那么它不应该有#pragma once :)同样,必须为该pragma定义某种理智的语义。如果你想多次包含同一个东西,不要把#pragma once放在里面:)
-
@KubaOber我只是要继续重复"编译器作者不要因为太过担心而无法解雇任何东西",直到全部影响陷入困境
-
@zwol我很抱歉,如果A / foo.h和B / foo.h是相同的并且包含警卫,那么必要时包含警卫也是相同的,无论宏定义是什么,第二个包含仍然会被忽略实际上,除非这些定义以某种方式改变了包含守卫的含义(他们可以吗?他们吗?)。对于预处理器"计算"目的而言,多次包含的Boost源不包括防护IIRC,它们对于它们来说是没有意义的。所以,除非我不了解你,否则我认为这不是一个真正的问题。
-
@KubaOber请接受为了假设有一个头文件foo.h应该只有一次,这取决于包含它的文件定义的宏。想象一下,这个头文件被复制到两个库A和B中,它们以不同的方式使用它,然后将这两个库复制到一个更大的程序中。在#ifndef守卫或你提议的#pragma一次性语义下,这将会破坏。但是对于#ifndef警卫,有一个明显的解决方案,维护较大程序的人可以应用而不影响其他任何事情:更改防护宏。
-
@KubaOber相比之下,对于你提出的#pragma一次性语义,还有一个修复 - 更改注释中的任何文本,比如说 - 但是从代码检查来看并不明显,维护者将不得不查找它手册,然后担心这是否适用于所有支持的编译器。因此,#ifndef guards在用户体验方面是更好的语言功能。
-
@zwol所以,解决方法始终是修改文件,除了包含警卫使其更明显?
-
@KubaOber对。但请注意,这仅适用于#pragma once的语义。面向路径名的语义可能需要您在发生冲突时重命名文件,这可能会破坏API合同。编译器在历史上一直不擅长记录他们如何实现这些东西,那么你怎么知道该怎么做?我正在考虑如何修改我的答案来更好地表达这些事情。
-
我认为没有办法让路径名以易于指定的方式工作,并且可以在随机平台上实现。如果要标准化#pragma once的行为,则甚至不应提及术语"路径"。包括守卫不使用路径,除了人类通常在那里设置一条受损的路径并且祈祷它不会与任何东西发生冲突。
-
你描述了pragma曾经失败的情况,但#ifndef也是如此。那么重点是什么呢?您必须更改文件的内容。如果你在任何地方使用prama,你可以将#ifndef添加到你的文件中,就是这样。如果您在任何地方都使用#ifndef,那么您可以更改它。如果您对某些文件有特殊需要,可以使用#ifndef,使用pragma一次就不会禁用这种可能性。
-
@fbucek他的观点是"你需要更改文件的内容"对于普通的包含保护更为明显,并且取决于特定编译器如何实现它,可能不足以保证#pragma once的成功。基本上,对于包括警卫,你可以一眼就看出你需要换一个守卫;但是,使用#pragma once,您需要查看编译器的文档以查看是否需要更改文件的内容,更改其名称或内容。
-
所以,实际上,它是一个有更明显的修复,并且逻辑修复可能实际上不足以支持另一个(取决于编译器)。
-
我从来没有遇到#pragma一次的问题。我只有#ifndef的问题。通常当我重命名文件然后用自动生成的#ifndef创建new。已经存在,因为我确实重命名了文件,而不是安全卫士。好的#pragma曾经有bug,我认为这真的很少见。它可以通过修改文件来解决。就像#ifndef的问题一样。我仍然看不出为什么不使用它们。没有什么是100%完美的。
-
GCC修复了文件名方法,并在发布此"回答"之前13年处理了正确的符号链接。即使没有文件名或inode,它仍然可以通过使用校验和来解决。编辑:gcc.gnu.org/bugzilla/show_bug.cgi?id=11569
-
@MickLH我建议你回应Jean-Michal Celerier的反应。他们认为他们修好了,但他们没有。
-
任何基于文件名的解决方案本身都是无用的浪费时间,不需要进一步讨论任何基于文件名或路径的解决方案。现在,文件的校验和不仅在具有时钟偏差的网络驱动器上保持一致,而且在具有完全不同名称和路径的重复项之间甚至是一致的。这正确地实现了#pragma once的唯一合理使用,即使用冗余包含树来组织数据结构定义。将此称为"不可修复的错误"是有偏见的或天真的,并且"任意不可预测的"路径路障是一个红色的鲱鱼。
-
@MickLH校验和不是一个选项,因为它们将破坏pragma的实际现有用法,这些用法期望更多类似路径名的语义。我还重申,作为编译器作者,无论您认为它们是多么疯狂,您都无法声明客户现有的代码库"不合理"。
-
那个前提是一个谬论!好吧,"不理智"是一种观点,所以你绝对可以宣称任何人的代码库是否理智。"一次"编译指示并非严格标准化,因此从技术上讲,任意选择都是有效的。正如你上面所说:你可能在几年前就已经把它撕掉了,肯定会破坏更多的代码库而不仅仅是切换到校验和。这个问题明确地要求减少冗余包含,这是编程中常见且有用的任务,而不是模糊滥用"曾经",正如你所说的,应该在很久以前就已经被破坏和重写了。
-
@MickLH欢迎您加入时间机器并回到2002年,并与我当时的老板讨论是否需要支持客户的代码。
-
为什么不能使用inode号? inode和设备编号的组合对于系统上的每个文件都是唯一的。
-
@fuz可悲的是,这不是真的;你不能指望在文件系统卸载和重新安装时保持稳定的inode或设备号(是的,这可能发生在编译运行的中间)。
-
@zwol如果在编译期间重新安装文件系统,则行为未定义,如果在编译期间更改任何文件,则行为未定义。这不是一个真正的问题。
-
@fuz就像我对MickLH说的那样,欢迎你加入你的时间机器并回到2002年与我当时的老板讨论是否需要支持客户的代码,除了s / code / build farm / 。
-
zwol我读了你链接的整个帖子,没有看到任何问题?所有看到的问题最终都得到了解决。
-
@ Jean-MichalCelerier我已经在上面的答案和评论中介绍了这一点。
-
@zwol如果你在gcc工作,我相信你,我正在接受你的建议,谢谢你。 (期待新的C ++"模块",我期望这些模块会让这些问题变得无声:-))
-
"只要没有两个标题定义相同的保护宏......"是一个无法修复的错误。
-
@DanielStevens如果两个标头定义了相同的保护宏,则可以重命名其中一个宏而不影响其他任何内容。如果两个标头的类型名称冲突导致#pragma once混淆,并且您坚持继续使用#pragma once,则必须重命名其中一个标头,这可能是API中断,因此无法实现。
-
这忽略了这一点。宏名称全局影响编译单元,即使标题来自单独开发的库,由不同的作者编写。库编写者无法合理地协调这样的重命名,从而迫使对库的用户进行更改。必须对第三方库进行源代码修改会大大增加使用这些库的维护负担。头部防护装置和#pragma once都不是合适的模块系统。
-
@DanielStevens是的,这些东西都不是一个合适的模块系统,但我认为你夸大了保持头部保护宏名称之间不相互冲突或者用于其他目的的名称的困难。如果我没记错的话,我在20年内遇到过两次这样的冲突......并且都没有涉及第三方库标题。另一方面,#pragma once故障是我整个工作期间一直存在的问题,因为建造农场的时钟不可靠(参见上文)。
直到#pragma once成为标准(当前不是未来标准的优先级)的那一天,我建议你使用它并使用警卫,这样:
1 2 3 4 5 6 7
| #ifndef BLAH_H
#define BLAH_H
#pragma once
// ...
#endif |
原因是:
-
#pragma once不是标准的,因此某些编译器可能不提供该功能。也就是说,所有主流编译器都支持它。如果编译器不知道它,至少它将被忽略。
-
由于#pragma once没有标准行为,因此您不应该假设所有编译器的行为都相同。警卫将至少确保所有编译器的基本假设是相同的,至少为警卫实施所需的预处理器指令。
-
在大多数编译器中,#pragma once将加速编译(一个cpp),因为编译器不会重新打开包含该指令的文件。因此,将其置于文件中可能有所帮助,具体取决于编译器。我听说g ++可以在检测到防护时进行相同的优化,但必须进行确认。
将两者结合使用,您可以获得每个编译器的最佳效果。
现在,如果你没有一些自动脚本来生成防护,那么使用#pragma once可能会更方便。只知道这对便携式代码意味着什么。 (我正在使用VAssistX快速生成警卫和编译指示)
您应该几乎总是以可移植的方式思考您的代码(因为您不知道将来会做什么)但是如果您真的认为它不是要用其他编译器编译(例如,非常特定的嵌入式硬件的代码)那么你应该检查一下#pragma once的编译器文档,知道你在做什么。
-
谁说#pragma曾经是标准的?此外,有多少编译器不跟踪头文件是否完全包含在包含保护中?换句话说 - 有人测量过#pragma曾经真正在现实中产生影响吗?
-
@Richard,我有,而且确实如此 - 我们的项目有5500包括大约1/2冗余。
-
@Richard是的性能已经过很多人的测试,优化至少存在于VC和gcc中。我没有说#pragma曾经是标准的,它只是一个非常大的可能性,因为标准化过程通常包括标准化被证明有效的常见功能/用法,比如#pragma once。
-
@ 280Z28:显然,5500的完全重建包括50%的冗余会花费一些成本。但是,你能提供数字,即。使用编译时一次编译时间(如果你愿意,请使用干净)与不使用它。如果你达到完全清洁构造的总补偿时间的1%以上,我会感到震惊。
-
@Klaim:你说的是 -"直到#pragma成为标准"。直到那意味着它将会发生,但这只是一个时间问题。但是,C ++'0x现在已经有很长一段时间了,并且已经有关于该功能标准化的常见新闻组的讨论。结论是存在一种现有机制,因此没有必要添加另一种做同样事情的方法。
-
@Klaim,280Z28:关于性能,请查看以下内容:gamesfromwithin.com/?p=32。你们中的任何一个都有一个真实项目的数字,显示项目的完整编译时间,有没有#pragma一次?
-
@Richard - 我会解决"直到"问题,因为它似乎并不明显,我只是想说,如果它有一天标准化,那么没有必要使用警卫。对于性能数字,我没有一些我目前的家庭项目,但我们做了一些改变一些公司源代码后使用pragma一次。编译器是VC9 / 9和CodeWarrior。我没有确切的成员,但我们有一些想法,比如10%的速度编译改进。我不确定这是否有趣,我想我们可以在网上找到数字或做一些测试是的,如果我找到足够的时间,我会试试。
-
顺便说一句,你提供的链接看起来是正确的,因为测试是在"旧"编译器上进行的,这个编译器已经足够老了,不能(Ithink)我们正在谈论的优化。最近的编译器(vc8 / 9,最后一个g ++)的新测试确实是一件好事,确保这一点。我同意我们应该提供数字,我现在没有。
-
"直到那意味着它将会发生,但这只是一个时间问题" - 不,它不会。这是一个虚假的迂腐案例;有人冒充专业知识,他们不仅缺乏专业知识,而且甚至没有应用过少量的思考或努力。而且我会相信这一点,直到他出现在我家门口并证明我错了。
-
包含守卫之后拥有#pragma once的原因是什么?
-
@DarioP:识别它的编译器不关心。当包含保护之外的东西时,不识别它的编译器有被禁用的风险包括保护优化。没有优化的编译器会在重复包含时跳过防护内部的所有内容,因此内部处理涉及的处理更少。
-
微软似乎已经优化了他们的编译器。来自Microsoft的VS2015文档:"在同一个文件中同时使用#include guard idiom和#pragma都没有优势。编译器识别#include守护成语,并以与#pragma一样的方式实现多重包含优化指令,如果在标准形式的成语之前或之后没有非注释代码或预处理器指令"msdn.microsoft.com/en-us/library/4141z1cx.aspx
-
这里的"#pragma once"永远不会被正确评估,因此多余,因为警卫将始终覆盖它。不要打扰,只需使用老式的后卫或只使用一次更简单的项目pragma。
从软件测试人员的角度来看
#pragma once比包含保护更短,更不容易出错,大多数编译器都支持,有些人说它编译速度更快(不再是[不再])。
但是我仍然建议你选择标准的#ifndef包括警卫。
为什么#ifndef?
考虑一个像这样的设计类层次结构,其中每个类A,B和C都存在于自己的文件中:
啊
1 2 3 4 5 6 7 8 9
| #ifndef A_H
#define A_H
class A {
public:
// some virtual functions
};
#endif |
b.h
1 2 3 4 5 6 7 8 9 10 11
| #ifndef B_H
#define B_H
#include"a.h"
class B : public A {
public:
// some functions
};
#endif |
c.h
1 2 3 4 5 6 7 8 9 10 11
| #ifndef C_H
#define C_H
#include"b.h"
class C : public B {
public:
// some functions
};
#endif |
现在让我们假设你正在为你的类编写测试,你需要模拟真正复杂的类B的行为。一种方法是使用例如google mock编写一个模拟类,并将其放在目录mocks/b.h中。请注意,类名称没有更改,但它只存储在不同的目录中。但最重要的是包含保护的名称与原始文件b.h中的名称完全相同。
嘲笑/ b.h
1 2 3 4 5 6 7 8 9 10 11 12 13
| #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 |
有什么好处?
使用这种方法,您可以模拟类B的行为,而无需触及原始类或告诉C它。您所要做的就是将目录mocks/放在编译器的包含路径中。
为什么不能用#pragma once来完成?
如果您使用#pragma once,则会出现名称冲突,因为它无法保护您无法定义类B两次,一次是原始版本,一次是模拟版本。
-
这对任何大型项目都不实用。保持B up2date会很痛苦,更不用说这违反了One Definition Rule
-
@parapurarajkumar ODR没有被违反,因为如果你包括mocks / bh之前bh预处理器将跳过bh完全让你只有一个B类。我想知道你是否有一个不那么"痛苦"的方法来测试B类。你采取什么策略?建议测试"任何大型项目"?
-
但是系统的其他部分,即非测试代码,你不能覆盖gmock.h?
-
没人压倒"gmock.h"。它是我们想要模拟一个类的Testframework的一部分。我想我们谈论的是两件不同的事情。重点是采用现有代码或类层次结构,并作为测试用模拟器或模拟类替换链的一部分。这样您就可以有效地测试非测试代码。它不像单元测试,你只测试一个类,但是模拟允许你做的是系统测试。如果你的类层次结构使用经典的#ifndef include guard,你可以在测试中用mock替换一个类。
-
嗯,如果原始b.h和mock b.h都在编译器中包含路径 - 那么#include"b.h"是否会出现名称冲突?如果从测试项目的include目录中删除原始的b.h路径,以便只拾取mock b.h,那么#pragma once是否也可以工作?
-
@Samaursa这些都是标题,我们还没有看到它们是如何包含在编译代码中的。我认为真正的cpp会#include"c.h",而测试代码会做类似#include"mocks/b.h"然后#include"c.h"的事情,或者真正的代码可能会#include 然后#include ,其中包含路径顺序。由于"而不是<>,c.h仍然包含本地b.h而不是mocks/b.h。但我可能错了。
-
通过在mocks / b.h中添加namespace mock_b {#include"b.h"}之类的代码来使用#pragma once时,也无法执行此操作。由于包含是预处理,忽略namespace,它会阻止a.h包含b.h并将实际的B类放在不同的命名空间中,而不会与模拟类B发生名称冲突
-
IMO如果要将mocks/b.h添加到包含路径,则还应删除原始b.h,而不是依赖包含保护。您的解决方案与符号插入一样蹩脚: - /
如果你肯定你永远不会在不支持它的编译器中使用这个代码(Windows / VS,GCC和Clang是支持它的编译器的例子),那么你当然可以使用#pragma一次而不用担心。
您也可以同时使用它们(参见下面的示例),以便在兼容系统上获得可移植性和编译加速
1 2 3 4 5 6 7
| #pragma once
#ifndef _HEADER_H_
#define _HEADER_H_
...
#endif |
-
#pragma曾经在许多不同的编译器(包括GCC)上得到很好的支持
-
是的,但并不是普遍支持的。始终支持包括警卫......取决于您希望代码的可移植性。
-
我真的不想要两者,只会让代码难看。只要便携性不是问题,我认为代码卫生问题是下一个问题。
-
即使您关心它,便携性也不是什么大不了的事。如果您目前正在编写仅适用于Windows的代码,那么代码中可能还有其他一些东西会比#pragma一次更紧密地绑定Windows。如果有一天你应该移植一些代码,那么编写一个遍历头文件的Perl脚本并用基于文件名和路径的include guard代替#pragma的所有用法并不困难。
-
另一方面,如果项目的一部分正在编写可能最终可在其他地方使用的位(例如数学库,api包装器等),那么就是可移植性。你担心你的代码应该如何看待,而不是关于它应该如何工作,你会担心...尤其是在人们已经学会忽视的文件的顶部。
-
@onebyone:从一开始就使用#pragma的唯一好处是潜在的性能提升。必须有一个显着的性能改进才能真正想要摆脱防止标题的多个包含的"标准"方式。默认情况下,清晰度应该出现在性能之前 - 在这种情况下,即使是性能优势也需要进行辩论。
-
以下划线开头的标识符保留给实现;不要在自己的代码中定义它们。
-
@KeithThompson"以下划线开头的标识符保留给实现" - 这太宽泛了。如果未跟随大写字母或其他下划线,则非全局标识符可以以下划线开头。
-
@JimBalter:我当然简化了一点 - 但恕我直言,遵循更一般的规则更容易也更明智,永远不会定义以下划线开头的标识符,而不是跟踪何时可以逃脱它。
-
@KeithThompson这个Q有一个C ++标签,C ++中的一个常见约定是例如以_开头的变量,因此过于宽泛的声明可能导致人们错误地认为这些违反了标准。无论如何,我不是在谈论实践,而是在谈论标准所说的事实。
-
对于它的价值 - Oracle Solaris Studio 12.3("Sun Pro C")不支持#pragma一次。我不记得收到警告,但我肯定有编译器错误。 :(
-
@JimBalter ISO C和ISO C ++都在标准库子句中使用了未定义的行为(在最近的C ++工作草案中,该规则被解除为整个语言,因为"不得使用","不需要诊断"声明,这在心理上与UB相同)。除非您确实知道自己在做什么(例如,您正在为标准库编写实现),否则使用这些保留标识符绝不是一种正确的做法。
-
@FrankHB变量名称不是"行为",所以你应该引用标准中的一些文字来说明这种无意义的主张。并且,正如我所说,使用下划线开始实例变量是一种普遍的做法,标准委员会也不会那么疯狂,无法使其失效。你对"这些保留的标识符"的引用表明你甚至没有读过我写的内容,这是关于"非全局标识符可以以下划线开头,如果没有后跟大写字母或另一个下划线",则不是和从未被保留过......"非全球化"是一个很大的线索。
-
@JimBalter True,名称不是行为,这是ISO C11 3.4指定的"外观或动作"。但是,这不允许使用名称。参见ISO C11 7.3和ISO C ++ 14 17.6.4.3,也是ISO C ++的工作文件。而"普遍"与"正确"无关。任何严肃的项目都会避免这种使用,除非它能通过其他方式获得可保证的可预测行为(例如像POSIX这样的语言标准的扩展)。注意,对于_ + UPPERCASE,"全局"是无关紧要的。此外,宏名称类似于"全局"。
-
JimBalter,我在谈论这个问题的用法。 @KeithThompson的陈述并不准确(从技术上讲,这是错误的,但基于陈述的误解不那么有害);我承认我当时没有仔细阅读。但是你在这个意义上对"标准所说的事实"的陈述也是不正确的,不管是不精确的陈述,直到你背诵"没有跟着大写字母或其他下划线"。还要注意这种情况先于"全球"。有了这个共识,我认为我们不再有分歧了。
-
"我承认我没有仔细阅读过它" - 的确,而且你还在继续。"直到你背诵" - 这就是我首先所说的。"我们不再有分歧" - 你的分歧是你自己建造的稻草人。"普遍"与"正确"无关 - 这就是自闭症观点,但在现实世界中,标准委员会(我自己在X3J11上)煞费苦心,广泛使用,例如_membername,仍然是正确的。现在我已经浪费了足够的时间在这个闲聊上关于一个3岁的评论,并且不会进一步参与。
在进行了关于#pragma once和#ifndef守卫之间假设的性能权衡与正确与否的论证之后的延伸讨论之后(我基于最近一些相对较近的灌输而采取了#pragma once的一面),决定最终测试#pragma once更快的理论,因为编译器不必尝试重新#include已经包含的文件。
对于测试,我自动生成了500个具有复杂相互依赖性的头文件,并且有一个.c文件#include。我以三种方式运行测试,一次仅使用#ifndef,一次使用#pragma once,一次使用两者。我在一个相当现代的系统上进行了测试(运行OSX的2014 MacBook Pro,使用XCode捆绑的Clang,内置SSD)。
一,测试代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57
| #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
#define _INCLUDE%d_H
", i, i);
#endif
#ifdef PRAGMA_ONCE
fprintf(fp,"#pragma once
");
#endif
for (j = 0; j < i; j++) {
fprintf(fp,"#include "include%d.h"
", j);
}
fprintf(fp,"int foo%d(void) { return %d; }
", i, i);
#ifdef IFNDEF_GUARD
fprintf(fp,"#endif
");
#endif
fclose(fp);
}
fp = fopen("main.c","w");
for (int i = 0; i < 100; i++) {
fprintf(fp,"#include "include%d.h"
", i);
}
fprintf(fp,"int main(void){int n;");
for (int i = 0; i < 100; i++) {
fprintf(fp,"n += foo%d();
", i);
}
fprintf(fp,"return n;}");
fclose(fp);
return 0;
} |
现在,我的各种测试运行:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57
| 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 |
正如您所看到的,带有#pragma once的版本确实比#ifndef仅稍微快一点,但差异可以忽略不计,并且会远远超过实际构建和链接代码的时间量。将采取。也许有足够大的代码库,它实际上可能导致几秒钟的构建时间差异,但现代编译器之间能够优化#ifndef防护,操作系统具有良好的磁盘缓存,以及存储技术的速度越来越快似乎性能论证没有实际意义,至少在当今这个典型的开发者系统中是如此。较旧且更具异国情调的构建环境(例如,托管在网络共享上的标头,从磁带构建等)可能会稍微改变等式,但在这些情况下,首先简单地制作不太脆弱的构建环境似乎更有用。
事实是,#ifndef标准化了标准行为,而#pragma once不是,而#ifndef也处理奇怪的文件系统和搜索路径极端情况,而#pragma once可能会因某些事情而感到困惑,导致错误程序员无法控制的行为。 #ifndef的主要问题是程序员为他们的警卫选择坏名称(名称冲突等),即使这样,API的使用者很可能使用#undef覆盖那些可怜的名字 - 这不是一个完美的解决方案,或许,但这是可能的,而如果编译器错误地剔除#include,则#pragma once无法追索。
因此,即使#pragma once明显(稍微)更快,我也不同意这本身就是将其用于#ifndef警卫的理由。
编辑:感谢来自@LightnessRacesInOrbit的反馈,我增加了头文件的数量,并将测试更改为仅运行预处理器步骤,消除了编译和链接过程中添加的任何少量时间(这在以前是微不足道的现在不存在)。正如所料,差异大致相同。
-
"微不足道"?你将构建时间缩短了一半。
-
@LightnessRacesinOrbit我将构建时间缩短了0.04秒。这恰好是一半的源文件,这是一个令人难以置信的错综复杂的#include规则的病态表现,可能永远不会出现在现实生活中。关键在于测量在#pragma once上应用了多少开销#ifndef,以及超过0.04秒的任何代码库都存在更大的问题。
-
您的答案提供的背景不足以判断随着项目的增长,节省的费用是否会保持0.04秒不变,或者随着项目的增长,或者介于两者之间的某个位置将保持不变。因此,它不是一个非常有用的基准,目前没有支持"微不足道"的结论。我只是提到它,因为我对真正的结论非常感兴趣!
-
这是一个好点 - 它只测量包含警卫的速度而不是预处理器的其余部分。我认为很明显,生成的代码只测试了这个方面,因为保护线之间没有很多。也许我可以扩展生成的代码,使每个文件有几千个随机生成的模板函数和类,这使得不同的不切实际的综合测试。
-
甚至不只是预处理器 - 您正在进行完整的编译和链接。我们无法从这些结果中知道"不相关的恒定开销"是什么,因此结果并没有真正告诉我们任何实际用途。可悲!
-
@LightnessRacesinOrbit我已经将测试更改为包含更多#include文件并且只运行预处理器。毫不奇怪,时间差大致相同(现在是一个小得多的分数)。
我通常不打扰#pragma once,因为我的代码有时必须使用除MSVC或GCC之外的其他东西进行编译(嵌入式系统的编译器并不总是使用#pragma)。
所以我不得不使用#include guards。我也可以使用#pragma once作为一些答案建议,但似乎没有太多理由,它经常会导致不支持它的编译器发出不必要的警告。
我不确定pragma可能会节省多少时间。我听说编译器通常已经识别出标题什么时候除了保护宏之外什么也没有注释,并且在那种情况下会做#pragma once等效(即,永远不再处理文件)。但我不确定它是否属实,或者仅仅是编译器可以进行优化的情况。
无论哪种情况,我都可以更轻松地使用#include警卫,它可以在任何地方使用,而不用担心它。
-
我很好奇哪个编译器不支持它。大多数编译器都会这样做,而我不知道哪些编译器没有。
我回答了一个相关的问题:
#pragma once does have one drawback (other than being non-standard) and that is if you have the same file in different locations (we have this because our build system copies files around) then the compiler will think these are different files.
我也在这里添加答案,以防有人绊倒这个问题,而不是另一个。
-
我认为这是不使用#pragma一次的最有说服力的理由。谢谢!
-
@ShitalShah如果您的构建系统复制文件arround,这只是一个考虑因素
-
修复#pragma once实现,它应该使用可靠的校验和而不是粗略的文件名。
我认为你应该做的第一件事是检查这是否真的会产生影响,即。你应该首先测试性能。其中一个谷歌搜索引发了这一点。
在结果页面中,对于我来说,这些列是关闭的,但很明显,至少VC6 microsoft没有实现其他工具正在使用的包含保护优化。如果包括后卫是内部的,那么包括后卫在外部的时间要长50倍(外部包括后卫至少和#pragma一样好)。但是让我们考虑一下这可能产生的影响:
根据提供的表格,打开包含和检查它的时间是#pragma等价物的50倍。但实际这样做的时间是在1999年以每张1微秒的速度测量的!
那么,单个TU有多少个重复的标题?这取决于你的风格,但如果我们说平均TU有100个重复,那么在1999年我们可能每个TU支付100微秒。随着HDD的改进,这可能会显着降低,但即使这样,使用预编译的头文件和正确的依赖关系跟踪,项目的总累积成本几乎肯定是构建时间的一个重要部分。
现在,另一方面,尽管不太可能,但如果您转移到不支持#pragma once的编译器,那么请考虑将整个源代码库更新为包含防护而不是#的时间需要多长时间编译?
没有理由微软无法以与GCC和其他所有编译器相同的方式实现包含保护优化(实际上任何人都可以确认他们的更新版本是否实现了这一点?)。除了限制你选择的替代编译器之外,恕我直言,#pragma once做的很少。
-
不要挑剔,但是如果你移植到一个非pragma-once支持编译器,编写一个关闭工具来拖网文件并用常规包含保护替换#pragma一次可能在整个移植过程中是微不足道的。
-
"实际上任何人都可以确认他们的更新版本是否实现了这一点?"在VS2015文档中提到它现在实现了包括保护优化。 msdn.microsoft.com/en-us/library/4141z1cx.aspx
-
"考虑需要花多少时间来更新整个源代码库以包含警卫而不是#pragma?"不应该花很长时间。这么容易就是为什么我写了guardonce。
#pragma once允许编译器在文件再次发生时完全跳过该文件 - 而不是解析文件,直到它到达#include警卫。
因此,语义稍有不同,但如果它们以预期的方式使用,它们是相同的。
将两者结合起来可能是最安全的路径,因为在最坏的情况下(编译器将未知的pragma标记为实际错误,而不仅仅是警告),您只需要删除#pragma本身。
当您将平台限制为"桌面上的主流编译器"时,您可以安全地省略#include警卫,但我也对此感到不安。
OT:如果你有关于加速版本的其他提示/经验,我会很好奇。
-
@Peterchen:声明编译器需要重新读取包含保护的文件是错误的。编译器第一次解析主体时,它可以记录标头是否包含正确的包含保护。因此,如果后面是#included,它可以跳过标题。关键的区别在于#pragma不是标准的,如果您需要使用不支持它的编译器,那么您将处于一个痛苦的世界。包括警卫在内可能发生的更糟糕的情况是性能受到轻微影响。
-
真的不是一个痛苦的世界。您可以相当轻松地编写一个脚本,用包含警卫替换所有出现的#pragma。可能发生的最糟糕的事情包括防御不是性能,它使用拼写错误的#ifdefs或复制文件,更改内容并忘记更新#ifdefs。
以上由Konrad Kleine解释。
简要总结:
-
当我们使用# pragma once时,它是编译器的责任,不允许多次包含它。这意味着,在您提及文件中的代码片段之后,它不再是您的责任。
现在,编译器会查找文件开头的代码片段,并将其从包含中删除(如果已包含一次)。这肯定会减少编译时间(平均而言,在庞大的系统中)。但是,在模拟/测试环境的情况下,由于循环等依赖性,将使测试用例实现变得困难。
-
现在,当我们使用#ifndef XYZ_H作为头文件时,开发人员有责任维护头文件的依赖性。这意味着,每当由于一些新的头文件,存在循环依赖的可能性,编译器将在编译时标记一些"undefined .."错误消息,并且用户检查实体的逻辑连接/流和纠正不当包括。
这肯定会增加编译时间(需要纠正和重新运行)。另外,因为它基于包含文件而工作,基于"XYZ_H"定义状态,并且仍然抱怨,如果不能获得所有定义。
因此,为避免这种情况,我们应该使用,作为;
1 2 3 4 5
| #pragma once
#ifndef XYZ_H
#define XYZ_H
...
#endif |
即两者的组合。
对于那些想要使用#pragma一次并且同时包含警卫的人:如果你没有使用MSVC,那么你将不会从#pragma获得一次优化。
并且你不应该将"#pragma once"放入一个应该被多次包含的标题中,每个包含可能具有不同的效果。
以下是关于#pragma一次使用的示例的详细讨论。
-
"而且你不应该将#pragma once放入一个应该被多次包含的标题中。" - 它适用于多次包含的标头;这就是重点。我认为你的意思是它不应该被用于多次被包含的标题,每个包含可能具有不同的效果。这方面的例子很少; 或就是这样一个例子(因为它的行为取决于是否定义了NDEBUG)。
-
谢谢,这更清楚了。更新了我的回答。