我正在开发一个已知只能在windows上运行并在Visual Studio下编译的代码库(它与excel紧密集成,所以它不会消失)。我想知道我是否应该使用传统的包含守卫或使用#pragma一次为我们的代码。我认为让编译器处理一次#pragma会产生更快的编译,并且在复制和粘贴时更不容易出错。它也稍微不那么丑陋;)
注意:为了获得更快的编译时间,我们可以使用冗余包含守卫,但这增加了包含文件和包含文件之间的紧密耦合。通常这是可以的,因为守卫应该基于文件名,并且只在你需要改变包含名称时才会改变。
我正在开发一个已知只能在windows上运行并在Visual Studio下编译的代码库(它与excel紧密集成,所以它不会消失)。我想知道我是否应该使用传统的包含守卫或使用#pragma一次为我们的代码。我认为让编译器处理一次#pragma会产生更快的编译,并且在复制和粘贴时更不容易出错。它也稍微不那么丑陋;)
注意:为了获得更快的编译时间,我们可以使用冗余包含守卫,但这增加了包含文件和包含文件之间的紧密耦合。通常这是可以的,因为守卫应该基于文件名,并且只在你需要改变包含名称时才会改变。
当前回答
从软件测试人员的角度来看
#pragma once比include守卫更短,更不容易出错,被大多数编译器支持,有人说它编译更快(这不是真的[不再])。
但我仍然建议你使用标准的#ifndef include守卫。
为什么ifndef ?
考虑这样一个人为的类层次结构,其中每个类a、B和C都存在于自己的文件中:
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编写一个模拟类,并将其放在目录mocks/b.h中。注意,类名没有改变,但它只存储在不同的目录中。但最重要的是,包含守护的名称与原始文件b.h中的名称完全相同。
mocks / 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一次性完成?
如果你只使用了一次#pragma,你就会得到一个名称冲突,因为它不能防止你定义类B两次,一次是原始版本,一次是模拟版本。
其他回答
我只是想补充一下这个讨论,我只是在VS和GCC上编译,并且习惯使用包含守卫。我现在已经切换到#pragma一次了,对我来说唯一的原因不是性能或可移植性或标准,因为我并不关心什么是标准,只要VS和GCC支持它,那就是:
#pragma一次性减少了出现错误的可能性。
将一个头文件复制并粘贴到另一个头文件,修改它以满足自己的需要,并且忘记更改include守卫的名称,这太容易了。一旦包含了两者,就需要花费一些时间来跟踪错误,因为错误消息并不一定是清晰的。
我不认为它会对编译时间产生重大影响,但#pragma once在所有编译器中都得到了很好的支持,但实际上并不是标准的一部分。预处理器可能会快一点,因为它更容易理解你的确切意图。
#pragma一次性使用不太容易出错,需要输入的代码也更少。
为了加快编译时间,尽可能地向前声明而不是包含在.h文件中。
我更喜欢使用#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当前版本中的实现仍然可能在合理的条件下失败,例如构建农场受到时钟倾斜的影响。我不知道其他编译器的实现是什么样的,但我不期望任何人做得更好。)
我认为你应该做的第一件事是看看这是否真的会产生影响。您应该首先测试性能。谷歌上的一个搜索结果是这样的。
在结果页面中,对我来说,列是缓慢的,但很明显,至少到VC6,微软没有实现其他工具正在使用的包括保护优化。当include守卫是内部的时候,它花费的时间是外部守卫的50倍(外部包含守卫至少和#pragma一样好)。但让我们考虑一下这可能产生的影响:
根据给出的表格,打开include并检查它的时间是使用#pragma的等效时间的50倍。但在1999年,每个文件的实际时间是1微秒!
那么,一个TU会有多少重复的头?这取决于你的风格,但如果我们说平均每个TU有100个副本,那么在1999年,我们可能要为每个TU支付100微秒。随着HDD的改进,现在这个数字可能会显著降低,但即使是这样,通过预编译头文件和正确的依赖关系,跟踪一个项目的累计成本几乎肯定是你构建时间中微不足道的一部分。
现在,在另一方面,可能不太可能,如果你曾经移动到一个不支持#pragma的编译器,那么考虑一下要花多少时间来更新你的整个源代码库,以包含守卫而不是#pragma?
微软没有理由不能像GCC和其他编译器一样实现包含保护优化(实际上有人能确认他们的最新版本是否实现了这一点吗?)恕我直言,#pragma once除了限制你对可选编译器的选择外,几乎没有什么作用。