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

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


当前回答

在Konrad Kleine的解释之上。

简要总结:

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

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

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

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

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

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

即两者的结合。

其他回答

#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一次并将守卫包含在一起的人:如果你不使用MSVC,那么你不会从#pragma一次得到太多优化。

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

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

直到#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的内容,以了解你真正在做什么。

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

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

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

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

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

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

我回答了一个相关的问题:

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

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