58

我目前正在做一个大项目,维护所有这些包括警卫让我抓狂!手写是令人沮丧的浪费时间。尽管许多编辑器可以生成包含守卫,但这并没有多大帮助:

  1. 编辑器根据文件名生成保护符号。当您在不同目录中具有相同文件名的标头时,会出现此问题。他们都将获得相同的包括后卫。将目录结构包含在保护符号中需要编辑器采取一些奇特的方法,因为宏中的斜杠和反斜杠并不是最好的选择。

  2. 当我必须重命名一个文件时,我也应该重命名所有包含保护(在 ifndef 中,定义最好是 endif 的注释)。恼人的。

  3. 预处理器充斥着大量符号,却不知道它们的含义。

  4. 然而定义被包含一次,编译器仍然在每次遇到头包含时打开头。

  5. 包含守卫不适合命名空间或模板。事实上,他们正在颠覆命名空间!

  6. 你有可能你的守卫符号不会是唯一的。

当程序在单个目录中包含少于 1000 个标头时,它们可能是可接受的解决方案。但如今呢?它很古老,与现代编码习惯无关。最困扰我的是,这个问题几乎可以通过#pragma once 指令完全解决。为什么不是标准?

4

9 回答 9

54

像这样的指令#pragma once以完全可移植的方式定义并不是一件容易的事,它具有明确的好处。它提出问题的一些概念在所有支持C.

当编译遇到 时#pragma once,它应该如何识别这个文件以便它不再包含它的内容?

显而易见的答案是文件在系统上的唯一位置。如果系统对所有文件都有唯一的位置,这很好,但许多系统提供链接(符号链接和硬链接),这意味着“文件”没有唯一的位置。是否应该仅仅因为通过不同的名称找到该文件而重新包含该文件?可能不是。

但是现在有一个问题,如何以#pragma once一种在所有平台上都具有确切含义的方式定义的行为——即使是那些甚至没有目录的平台,更不用说符号链接——并且仍然在系统上获得理想的行为有吗?

你可以说一个文件的身份是由它的内容决定的,所以如果一个包含的文件有一个#pragma once和一个包含的文件具有完全相同的内容,那么第二个和后续#include的 s 将无效。

这很容易定义并且具有明确定义的语义。它还具有良好的属性,例如,如果将项目从支持和使用文件系统链接的系统移动到不支持和使用文件系统链接的系统,它的行为仍然相同。

不利的一面是,每次遇到包含其内容的包含文件时,都必须与迄今为止已包含的#pragma once所有其他文件进行检查。#pragma once这意味着在任何情况下都类似于使用#include守卫的性能损失,并给编译器编写者增加了不小的负担。显然,这个结果可以被缓存,但对于传统的包含守卫也是如此。

传统的包含保护强制程序员选择一个宏作为包含文件的唯一标识符,但至少该行为是定义良好且易于实现的。

考虑到潜在的陷阱和成本,以及传统的 include 保护确实有效的事实,标准委员会认为没有必要进行标准化对我来说并不奇怪#pragma once

于 2009-11-08T12:07:47.930 回答
19

包含守卫绝对是个麻烦事,而 C 最初应该被设计成默认情况下包含一次标头 - 需要一些特殊选项来多次包含标头。

然而,事实并非如此,而且你大多被困在必须使用包含警卫的情况下。也就是说,#pragma once它得到了广泛的支持,因此您可能可以不用使用它。

就个人而言,我通过将 GUID 添加到包含保护来解决您的第一个问题(类似地命名为包含文件)。它很丑,而且大多数人都讨厌它(所以我经常被迫在工作中不使用它),但你的经验表明这个想法有一定的价值——即使它非常丑陋(但话说回来,整个包含保护的东西是一种 hack - 为什么不全力以赴?):

#ifndef C_ASSERT_H_3803b949_b422_4377_8713_ce606f29d546
#define C_ASSERT_H_3803b949_b422_4377_8713_ce606f29d546

// blah blah blah...

#endif

我听说编译器实际上并没有重新打开包含保护的头文件(他们已经学会了识别成语)。我不确定这是否属实(或在多大程度上属实);我从来没有测量过。我也不担心它,但我的项目并没有那么大,这是一个问题。

我的 GUID hack 几乎解决了第 1、5 和 6 项。我只使用第 2、3 和 4 项。实际上,对于第 2 项,您可以在重命名文件时不重命名包含保护宏,因为 GUID 将确保它仍然是独一无二的。事实上,根本没有理由将文件名与 GUID 合并。但我愿意——我想是传统。

于 2009-11-08T09:05:01.840 回答
14
  1. 您需要多久向该项目添加一次包含文件?将 DIRNAME_FILENAME 添加到警卫中真的有那么难吗?并且总是有 GUID。
  2. 你真的经常重命名文件吗?曾经?此外,将 GUARD 放在#endif 中与任何其他无用的注释一样烦人。
  3. 我怀疑您的 1000 个头文件保护定义甚至只是系统库生成的定义数量的一小部分(尤其是在 Windows 上)。
  4. 我认为 DOS 的 MSC 10(20 多年前)会跟踪包含哪些标题,如果它们包含警卫,如果再次包含,则会跳过它们。这是老技术。
  5. 命名空间和模板不应跨越标题。亲爱的,不要告诉我你这样做:

    template <typename foo>
    class bar {
    #include "bar_impl.h"
    };
    
  6. 你已经说过了。

于 2009-11-08T09:22:41.197 回答
9

如前所述,C++ 标准应考虑不同的开发平台,其中一些可能存在限制,使得 #pragma once 支持无法实现。

另一方面,由于类似的原因,之前没有添加对线程的支持,但较新的 C++ 标准仍然包含线程。在后一种情况下,我们可以为一个非常有限的平台进行交叉编译,但开发是在一个成熟的平台上完成的。由于 GCC 支持这个扩展,我认为,你的问题的真正答案是没有兴趣将这个特性推入 C++ 标准。

从实际的角度来看,包含守卫比不遵守#pragma once 指令给我们的团队带来的麻烦更大。例如,如果文件重复并且随后两个副本都包含在内,则包含守卫中的 GUID 没有帮助。当只使用#pragma 时,一旦我们得到重复定义错误并且可以花时间统一源代码。但是在包含守卫的情况下,问题可能需要运行时测试才能捕获,例如,如果函数参数的默认参数的副本不同,就会发生这种情况。

我避免使用包含警卫。如果我必须将我的代码移植到没有#pragma once 支持的编译器,我将编写一个脚本,该脚本将为所有头文件添加包含保护。

于 2011-08-08T07:52:53.003 回答
4

当您在不同目录中具有相同文件名的标头时,会出现此问题。

所以你有两个ice_cream_maker.h在你的项目中都被调用的头文件,它们都有一个ice_cream_maker在它们中定义的类,它执行相同的功能?或者你正在调用你系统中的每一个类foo

然而定义被包含一次,编译器仍然在每次遇到头包含时打开头。

编辑代码,这样您就不会多次包含标题。

对于依赖标头(而不是库的主标头),我经常使用如下标头保护:

#ifdef FOO_BAR_BAZ_H
#error foo_bar_baz.h multiply included
#else
#define FOO_BAR_BAZ_H

// header body

#endif
于 2009-11-08T09:55:56.080 回答
3

IIRC,#pragma任何东西都不是语言的一部分。它在实践中出现了很多。

(编辑:完全同意包含和链接系统应该更多地关注新标准,因为它是“当今时代”中最明显的弱点之一)

于 2009-11-08T10:23:14.177 回答
2

通过将包含保护设置为包含文件中的类和名称空间的名称,您可能可以避免名称冲突而无需使用随机字符串。

此外,MS 编译器和 GCC 都支持 #pragma once 已经有一段时间了,那么为什么它不在 ISO 标准上会打扰你呢?

于 2009-11-08T09:10:37.010 回答
2

一个务实的解决方案:

1)选择一些一致的保护命名策略(例如,相对于项目根目录+文件名的路径,或者您选择的任何其他内容)。包括第三方代码的可能例外。

2)编写一个程序(一个简单的python脚本就可以)递归地遍历源树并验证守卫是否都符合策略。并且每当警卫出错时,输出用户可以轻松应用修复的差异(或 sed 脚本或其他任何内容)。或者只是要求确认并从同一程序进行更改。

3)让项目中的每个人都使用它(例如,在提交到源代码控制之前)。

于 2009-11-08T10:44:11.207 回答
1

我认为正确的方法是允许仅使用特殊的 pragma 进行多次包含,并且默认情况下不允许多次包含,例如:

#pragma allow_multiple_include_this_file

所以既然你问为什么。您是否将您的提案发送给标准开发人员?:) 我也不送。可以是一个原因吗?

于 2012-01-13T04:28:46.793 回答