#pragma曾经vs包括守卫?

时间:2009-07-17 15:18:03

标签: c++ coding-style

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

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

13 个答案:

答案 0 :(得分:281)

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

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

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

我更喜欢使用#pragma once

wikipedia article about the possibility of using both

答案 1 :(得分:156)

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

#pragma once减少了错误的可能性。

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

答案 2 :(得分:98)

LIKE()无法修复的错误。永远不应该使用它。

如果您的#pragma once搜索路径足够复杂,编译器可能无法区分具有相同基本名称的两个标头之间的区别(例如#includea/foo.h),因此{ {1}}其中一个会禁止两个。它也可能无法分辨两个不同的相对包含(例如b/foo.h#pragma once引用相同的文件,因此#include "foo.h"将无法抑制冗余包含它应该具有的内容。< / p>

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

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

(为什么这是 unfixable 的简短版本是Unix和Windows文件系统API都没有提供保证的任何机制来告诉你两个绝对路径名是否指的是同一个文件。如果你的印象是可以使用inode编号,抱歉,你错了。)

(历史记录:大约12年前,当我有权这样做时,我没有从GCC中删除#pragma once#pragma once的唯一原因是Apple的系统标题依赖于它们回想起来,这不应该阻止我。)

(因为现在评论主题中出现了两次:GCC开发人员确实尽可能地使#pragma once尽可能可靠;请参阅GCC bug report 11569。但是,实施中当前版本的GCC 仍然可能在合理的条件下失败,例如遭受时钟偏差的构建农场。我不知道其他编译器的实现是什么样的,但我不希望任何人做过更好。)

答案 3 :(得分:33)

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

#ifndef BLAH_H
#define BLAH_H
#pragma once

// ...

#endif

原因是:

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

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

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

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

答案 4 :(得分:30)

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

#pragma once比包含保护更短,更不容易出错,大多数编译器支持,有些人说它编译速度更快(不再是[不再])。

但我仍然建议您使用标准#ifndef包括警卫。

为什么#ifndef

考虑一个像这样的设计类层次结构,其中每个类ABC都存在于其自己的文件中:

A.H

#ifndef A_H
#define A_H

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

#endif

b.h

#ifndef B_H
#define B_H

#include "a.h"

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

#endif

c.h

#ifndef C_H
#define C_H

#include "b.h"

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

#endif

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

嘲笑/ b.h

#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两次,一次是原始版本,一次是模拟版本。

答案 5 :(得分:22)

如果你肯定你永远不会在不支持它的编译器中使用这个代码(Windows / VS,GCC和Clang是支持它的编译器的例子),然后你可以毫无后顾之忧地使用#pragma。

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

#pragma once
#ifndef _HEADER_H_
#define _HEADER_H_

...

#endif

答案 6 :(得分:17)

在进行了关于#pragma once#ifndef守卫与正确性论证之间假设的性能权衡的扩展讨论之后(我在#pragma once的基础上采用了一些因此,我决定最终测试#pragma once更快的理论,因为编译器不必尝试重新#include已经包含的文件。

对于测试,我自动生成了500个具有复杂相互依赖性的头文件,并且.c文件#include全部都是#ifndef。我以三种方式运行测试,一次只有#pragma once,一次只有#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; } ,一次只有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 。我在一个相当现代的系统上进行了测试(运行OSX的2014 MacBook Pro,使用XCode捆绑的Clang和内部SSD)。

首先,测试代码:

#pragma once

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

#ifndef

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

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

因此,即使 ;Zanglang .model small .stack 200h init macro mov ax, @data mov ds, ax endm fin macro mov ah, 04ch int 21h endm print macro str mov ah, 9h lea dx, str int 21h endm strinp macro mov ah, 0Ah mov dx, offset buff int 21h mov si, offset buff + 1 mov cl, [ si ] mov ch, 0 inc cx add si, cx mov al, '$' mov [ si ], al ;Print the value of buff print newl print buff + 2 print newl ;This works!!! mov ah, 9h mov dx, offset buff + 2 int 21h endm .data prompt db 10, 13, "Enter your string: $" newl db 10, 13, "$" buff db 26 db ? db 26 dup(0) .code start: init print prompt strinp fin end start end 显然(稍微)更快,我也不同意这本身就是kubectl守卫使用它的理由

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

答案 7 :(得分:15)

我通常不会打扰#pragma once,因为我的代码有时必须使用MSVC或GCC之外的其他东西编译(嵌入式系统的编译器并不总是有#pragma)。

所以我必须使用#include guards。我也可以使用#pragma once作为一些答案建议,但似乎没有太多理由,它往往会对不支持它的编译器造成不必要的警告。

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

在任何一种情况下,我都可以更轻松地使用#include警卫,它可以在任何地方使用,而不用担心它。

答案 8 :(得分:10)

related question

I answered
  

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

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

答案 9 :(得分:9)

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

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

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

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

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

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

答案 10 :(得分:4)

#pragma once允许编译器在文件再次发生时完全跳过该文件 - 而不是解析文件,直到它到达#include警卫。

因此,语义略有不同,但如果它们以预期的方式使用,它们是相同的。

将两者结合起来可能是最安全的路径,因为在最坏的情况下(编译器将未知的pragma标记为实际错误,而不仅仅是警告),你只需要删除#pragma本身。

当您将平台限制为“桌面上的主流编译器”时,您可以安全地省略#include警卫,但我也感到不安。

OT:如果你有关于加速版本的其他提示/经验,我会很好奇。

答案 11 :(得分:1)

对于那些想要使用#pragma一次并且同时包含警卫的人:如果你没有使用MSVC,那么你将不会从#pragma中获得一次优化。

你不应该将“#pragma once”放入一个应该多次包含的标题中,每个包含可能会产生不同的效果。

Here详细讨论了#pragma一次使用的例子。

答案 12 :(得分:1)

上面的Konrad Kleine解释。

简要总结:

  • 当我们使用# pragma once时,它是编译器的责任,不允许多次包含它。这意味着,在您提及文件中的代码片段之后,不再是您的责任。

现在,编译器会查找文件开头的代码片段,并将其从包含中删除(如果已包含一次)。这肯定会减少编译时间(平均而言,在庞大的系统中)。但是,在模拟/测试环境的情况下,由于循环等依赖性,将使测试用例实现变得困难。

  • 现在,当我们使用#ifndef XYZ_H作为标头时,开发人员更有责任维护标头的依赖性。这意味着,无论何时由于某些新的头文件,都存在循环依赖的可能性,编译器只会标记一些&#34; undefined ..&#34;编译时出错,用户检查实体的逻辑连接/流程并纠正不正确的包含。

这肯定会增加编译时间(需要纠正和重新运行)。此外,因为它的工作基于包含文件,基于&#34; XYZ_H&#34;定义状态,如果不能得到所有定义,仍然会抱怨。

因此,为了避免这种情况,我们应该使用as;

#pragma once
#ifndef XYZ_H
#define XYZ_H
...
#endif

即。两者的结合。