#pragma once vs include guards?

我正在开发一个已知只能在windows上运行并在Visual Studio下编译的代码库(它与excel紧密集成,所以它不会消失)。我想知道我是否应该使用传统的包含守卫或使用#pragma once为我们的代码。我认为让编译器处理#pragma once将产生更快的编译,并且在复制和粘贴时更不容易出错。它也稍微不那么丑陋;)

注意:为了获得更快的编译时间,我们可以使用冗余包括保护,但这增加了包含文件和包含文件之间的紧密耦合。通常这是可以的,因为守卫应该基于文件名,并且只在你需要改变包含名称时才会改变。

314769 次浏览

我不认为它会对编译时间产生重大影响,但#pragma once在所有编译器中都得到了很好的支持,但实际上不是标准的一部分。预处理器可能会快一点,因为它更容易理解你的确切意图。

#pragma once不太容易出错,需要输入的代码也更少。

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

我更喜欢使用#pragma once

请看这个维基百科上的一篇文章提到了两者同时使用的可能性

如果你确定你永远不会在不支持它的编译器中使用这段代码(Windows/VS, GCC和Clang是支持它的编译器的例子),那么你当然可以使用#pragma一次而不用担心。

您也可以两者都使用(参见下面的示例),这样就可以在兼容系统上获得可移植性和编译加速

#pragma once
#ifndef _HEADER_H_
#define _HEADER_H_


...


#endif

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

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

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

当你限制你的平台,比如说“桌面上的主流编译器”,你可以安全地省略#include守卫,但我在这方面也感到不安。

OT:如果你有其他关于加速构建的技巧或经验可以分享,我很好奇。

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

#ifndef BLAH_H
#define BLAH_H
#pragma once


// ...


#endif

原因如下:

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

同时使用这两种编译器,可以充分利用每种编译器的优点。

现在,如果你没有一些自动脚本来生成守卫,那么使用#pragma once可能会更方便。要知道这对可移植代码意味着什么。(我使用VAssistX生成守卫和pragma一次快速)

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

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

所以我必须使用#include守卫。我也可以像一些答案建议的那样使用#pragma once,但似乎没有太多理由,而且它经常会对不支持它的编译器造成不必要的警告。

我不确定这种务实会节省多少时间。我听说编译器通常已经识别出当一个头文件除了守卫宏之外没有任何注释时,并在这种情况下执行等效#pragma once(即。,不再处理该文件)。但我不确定这是真的还是编译器可以做这个优化的情况。

在任何一种情况下,对我来说使用#include守卫更容易,它将在任何地方工作,而不用再担心它了。

我认为你应该做的第一件事是看看这是否真的会产生影响。您应该首先测试性能。谷歌中的一个搜索抛出了

在结果页面中,对我来说,列是缓慢的,但很明显,至少到VC6,微软没有实现其他工具正在使用的包括保护优化。当include守卫是内部的时候,它花费的时间是外部守卫的50倍(外部包含守卫至少和#pragma一样好)。但让我们考虑一下这可能产生的影响:

根据给出的表格,打开include并检查它的时间是使用#pragma的等效时间的50倍。但在1999年,每个文件的实际时间是1微秒!

那么,一个TU会有多少重复的头?这取决于你的风格,但如果我们说平均每个TU有100个副本,那么在1999年,我们可能要为每个TU支付100微秒。随着HDD的改进,现在这个数字可能会显著降低,但即使是这样,通过预编译头文件和正确的依赖关系,跟踪一个项目的累计成本几乎肯定是你构建时间中微不足道的一部分。

现在,在另一方面,这可能是不太可能的,如果你曾经移动到一个不支持#pragma once的编译器,那么考虑一下要花多少时间来更新你的整个源代码库,以包含守卫而不是#pragma?

微软没有理由不能像GCC和其他编译器一样实现包含保护优化(实际上有人能确认他们的最新版本是否实现了这一点吗?)恕我直言,#pragma once除了限制你对可选编译器的选择外,几乎没有什么作用。

我只是想补充一下这个讨论,我只是在VS和GCC上编译,并且习惯使用包含守卫。我现在已经切换到#pragma once,对我来说唯一的原因不是性能或可移植性或标准,因为我并不真正关心什么是标准,只要VS和GCC支持它,那就是:

#pragma once减少了出现bug的可能性。

将一个头文件复制并粘贴到另一个头文件,修改它以满足自己的需要,并且忘记更改include守卫的名称,这太容易了。一旦包含了两者,就需要花费一些时间来跟踪错误,因为错误消息并不一定是清晰的。

对于那些想使用#pragma一次并将守卫包含在一起的人:如果你不使用MSVC,那么你不会从#pragma一次得到太多优化。

你不应该把“#pragma once”放在一个应该被包含多次的头文件中,因为每次包含可能会产生不同的效果。

在这里是一个关于#pragma用法的详细讨论示例。

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

#pragma once比include守卫更短,更不容易出错,被大多数编译器支持,有些人说它编译更快(这不是真的[不再])。

但是我仍然建议你使用标准的#ifndef include守卫。

为什么#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

刘昀

#ifndef C_H
#define C_H


#include "b.h"


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


#endif

现在让我们假设你正在为你的类编写测试,你需要模拟真正复杂的类B的行为。一种方法是使用谷歌的模拟编写模拟类,并将其放入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/放在你的编译器的include路径中。

为什么不能用#pragma once来实现呢?

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

上面由康拉德Kleine解释。

简要总结:

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

现在,编译器在文件的开头寻找这个代码片段,并跳过它不被包含(如果已经包含一次)。这肯定会减少编译时间(在一般和大型系统中)。然而,在模拟/测试环境中,由于循环等依赖关系,将使测试用例的实现变得困难。

  • 现在,当我们为头文件使用#ifndef XYZ_H时,维护头文件的依赖关系更多地是开发人员的责任。这意味着,每当由于一些新的头文件,存在循环依赖关系的可能性时,编译器只会在编译时标记一些“undefined ..”错误消息,并且由用户检查实体的逻辑连接/流并纠正不适当的包含。

这肯定会增加编译时间(因为需要纠正和重新运行)。此外,由于它是在包含文件的基础上工作的,基于“XYZ_H”定义状态,如果不能获得所有定义,仍然会报错。

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

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

即两者的结合。

有一个相关的问题,它的我回答:

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

我把答案也加在这里,以防有人被这个问题绊倒,而不是其他问题。

#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年前,当我有权从GCC中删除#pragma once#import时,我没有这样做的唯一原因是Apple的系统头文件依赖于它们。现在回想起来,这不应该阻止我。)

(因为这已经在评论线程中出现了两次:GCC开发人员确实付出了相当多的努力来使#pragma once尽可能可靠;看到GCC错误报告11569。然而,当前版本GCC 仍然可能失败的实现在合理的条件下,例如建立农场遭受时钟倾斜。我不知道任何其他编译器的实现是什么样的,但我不希望任何人做更好的。)

在进行了关于#pragma once#ifndef守卫之间假定的性能权衡与正确与否的争论(我基于最近的一些相关灌输而站在#pragma once一边)的扩展讨论之后,我决定最终测试这个理论,即#pragma once更快,因为编译器不必试图重新__abc4一个已经包含的文件。

对于测试,我自动生成了500个相互依赖复杂的头文件,并有一个.c文件将它们全部#includes。我用三种方式运行测试,一次只用#ifndef,一次只用#pragma once,还有一次两者都用。我在一个相当现代化的系统上进行了测试(一台2014年的MacBook Pro,运行OSX,使用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;
}

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

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的版本确实比只有__abc1的版本的预处理速度略快,的差异是可以忽略不计的,并且与实际构建和链接代码所花费的时间相比要小得多。也许有了足够大的代码库,构建时间可能会有几秒钟的差异,但现代编译器能够优化#ifndef保护,操作系统有良好的磁盘缓存,以及存储技术的速度不断提高,似乎性能的争论是没有意义的,至少在当今时代的典型开发系统上是这样。旧的和更奇特的构建环境(例如托管在网络共享上的头文件,从磁带构建等等)可能会在某种程度上改变这种平衡,但在这些情况下,首先创建一个不那么脆弱的构建环境似乎更有用。

事实是,#ifndef是标准行为的标准化,而#pragma once不是,并且#ifndef还处理奇怪的文件系统和搜索路径的角落情况,而#pragma once可能会被某些事情弄得非常混乱,导致程序员无法控制的不正确行为。#ifndef的主要问题是程序员为他们的守卫选择了糟糕的名字(有名称冲突等),即使这样,API的消费者也很有可能使用#undef来覆盖这些糟糕的名字——也许不是一个完美的解决方案,但它是可能的,而如果编译器错误地挑选了#include#pragma once就没有追追权。

因此,尽管 #pragma once明显(稍微)更快,我不同意这本身是使用它而不是#ifndef守卫的理由。

增加头文件的数量并将测试更改为只运行预处理器步骤消除了编译和链接过程所增加的少量时间(这在以前是微不足道的,现在已经不存在了)。不出所料,两者之间的差异大致相同。