关于c ++:#pragma曾经vs包含守卫?

#pragma once vs include guards?

本问题已经有最佳答案,请猛点这里访问。

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

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


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

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

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

我更喜欢使用#pragma once

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


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

#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成为标准(当前不是未来标准的优先级)的那一天,我建议你使用它并使用警卫,这样:

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 once比包含保护更短,更不容易出错,大多数编译器都支持,有些人说它编译速度更快(不再是[不再])。

但是我仍然建议你选择标准的#ifndef包括警卫。

为什么#ifndef

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

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


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

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

1
2
3
4
5
6
7
#pragma once
#ifndef _HEADER_H_
#define _HEADER_H_

...

#endif


在进行了关于#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的反馈,我增加了头文件的数量,并将测试更改为仅运行预处理器步骤,消除了编译和链接过程中添加的任何少量时间(这在以前是微不足道的现在不存在)。正如所料,差异大致相同。


我通常不打扰#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.

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


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

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

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

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

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

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


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

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

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

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

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


以上由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一次使用的示例的详细讨论。