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

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


当前回答

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

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

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

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

其他回答

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

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

下面是关于#pragma用法的详细讨论和示例。

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

#pragma一次性减少了出现错误的可能性。

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

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

对于测试,我自动生成了500个相互依赖复杂的头文件,并有一个包含它们的.c文件。我用三种方式运行测试,一次只用#ifndef,一次只用#pragma,一次两种都用。我在一个相当现代化的系统上进行了测试(一台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

As you can see, the versions with #pragma once were indeed slightly faster to preprocess than the #ifndef-only one, but the difference was quite negligible, and would be far overshadowed by the amount of time that actually building and linking the code would take. Perhaps with a large enough codebase it might actually lead to a difference in build times of a few seconds, but between modern compilers being able to optimize #ifndef guards, the fact that OSes have good disk caches, and the increasing speeds of storage technology, it seems that the performance argument is moot, at least on a typical developer system in this day and age. Older and more exotic build environments (e.g. headers hosted on a network share, building from tape, etc.) may change the equation somewhat but in those circumstances it seems more useful to simply make a less fragile build environment in the first place.

The fact of the matter is, #ifndef is standardized with standard behavior whereas #pragma once is not, and #ifndef also handles weird filesystem and search path corner cases whereas #pragma once can get very confused by certain things, leading to incorrect behavior which the programmer has no control over. The main problem with #ifndef is programmers choosing bad names for their guards (with name collisions and so on) and even then it's quite possible for the consumer of an API to override those poor names using #undef - not a perfect solution, perhaps, but it's possible, whereas #pragma once has no recourse if the compiler is erroneously culling an #include.

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

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

#pragma曾经有无法修复的错误。它不应该被使用。

如果你的#include搜索路径足够复杂,编译器可能无法区分两个具有相同基底名的头文件(例如a/foo.h和b/foo.h),因此在其中一个头文件中使用#pragma一次将同时屏蔽两个头文件。它也可能无法区分两个不同的相对include(例如#include "foo.h"和#include "../a/foo.h"指的是同一个文件,因此#pragma once将无法在应该包含冗余include的情况下抑制冗余include。

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

#pragma曾经没有这样的安全网——如果编译器对头文件的标识错误,无论哪种情况,程序都将编译失败。如果你遇到了这个错误,你唯一的选择是停止使用#pragma一次,或者重命名一个头文件。头文件的名称是API契约的一部分,因此重命名可能不是一个选项。

(简而言之,这是不可修复的,因为Unix和Windows文件系统API都没有提供任何机制来保证告诉你两个绝对路径名是否指向同一个文件。如果您认为inode编号可以用于此,那么对不起,您错了。)

(历史注:12年前,当我有权从GCC中删除#pragma和#import时,我没有这样做的唯一原因是苹果的系统头文件依赖于它们。现在回想起来,这不应该阻止我。)

(因为这个问题已经在评论中出现了两次:GCC开发人员确实花了很多精力让#pragma once尽可能可靠;参见GCC bug报告11569。然而,GCC当前版本中的实现仍然可能在合理的条件下失败,例如构建农场受到时钟倾斜的影响。我不知道其他编译器的实现是什么样的,但我不期望任何人做得更好。)

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

#ifndef BLAH_H
#define BLAH_H
#pragma once

// ...

#endif

原因如下:

#pragma once is not standard, so it is possible that some compiler don't provide the functionality. That said, all major compiler supports it. If a compiler don't know it, at least it will be ignored. As there is no standard behavior for #pragma once, you shouldn't assume that the behavior will be the same on all compiler. The guards will ensure at least that the basic assumption is the same for all compilers that at least implement the needed preprocessor instructions for guards. On most compilers, #pragma once will speed up compilation (of one cpp) because the compiler will not reopen the file containing this instruction. So having it in a file might help, or not, depending on the compiler. I heard g++ can do the same optimization when guards are detected but it have to be confirmed.

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

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

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