我目前正在开展一个大型项目并且维护所有这些包括警卫让我疯狂!手工编写是令人沮丧的浪费时间。虽然许多编辑可以生成包含警卫但这没有多大帮助:
编辑器根据文件名生成保护符号。如果在不同目录中具有相同文件名的标头,则会出现此问题。他们两个都会得到相同的包括后卫。将目录结构包含在保护符号中需要编辑器的一些奇特的方法,因为宏中的斜杠和反斜杠不是最好的。
当我必须重命名文件时,我应该重命名所有包含警戒(在ifndef,定义和理想的endif的注释中)。烦。
预处理程序充满了大量的符号,却没有任何线索的含义。
然而,定义包含一次,编译器每次遇到标题包含时仍会打开标题。
包含防护不适合命名空间或模板。实际上他们正在颠覆命名空间!
您的警卫符号可能不是唯一的。
当程序在单个目录中包含少于1000个标头时,它们可能是可接受的解决方案。但是现在呢?它很古老,与现代编码习惯无关。令我困扰的是,这个问题几乎可以通过#pragma once指令完全解决。为什么它不是标准?
答案 0 :(得分:49)
像#pragma once
这样的指令,以完全可移植的方式定义,并没有明确的明确好处。它提出问题的一些概念在支持C
的所有系统上都没有很好地定义,并且以一种简单的方式定义它可能没有比传统的包含守卫更有利。
当编译遇到#pragma once
时,如何识别此文件以使其不再包含其内容?
显而易见的答案是文件在系统上的唯一位置。如果系统具有所有文件的唯一位置,但很多系统提供的链接(符号链接和硬链接)意味着“文件”没有唯一的位置,那么这很好。该文件是否应该重新包含,因为它是通过不同的名称找到的?可能不是。
但是现在有一个问题,如何以一种在所有平台上具有确切含义的方式定义#pragma once
的行为是可能的 - 甚至那些甚至没有目录的人,更不用说符号链接了 - 并且仍然在拥有它们的系统上获得理想的行为?
您可以说文件标识由其内容决定,因此如果包含的文件包含#pragma once
并且包含的文件完全相同的内容,那么第二个随后的#include
将无效。
这很容易定义,并且具有良好定义的语义。它还具有良好的属性,如果项目从支持和使用文件系统链接的系统移动到不支持和使用的系统,它仍然表现相同。
缺点是,每次遇到包含#pragma once
的包含文件时,必须使用目前已包含的#pragma once
对每个其他文件检查其内容。这意味着在任何情况下都会遇到类似于使用#include
警卫的性能损失,并为编译器编写者增加了不小的负担。显然,这个结果可以缓存,但传统的包含守卫也是如此。
常规包括警卫强制程序员选择一个宏作为包含文件的唯一标识符,但至少行为定义明确且易于实现。
鉴于潜在的陷阱和成本,以及传统包括警卫确实有效的事实,标准委员会认为没有必要标准化#pragma once
并不奇怪。
答案 1 :(得分:19)
包含警卫绝对是一种烦恼,C本来应该设计为默认情况下会包含一次标题 - 需要一些特殊选项来多次包含标题。
然而,事实并非如此,而且你大部分都不得不使用包含警卫。也就是说,#pragma once
得到了广泛支持,因此您可以放弃使用它。
就个人而言,我通过向包含守卫添加GUID来解决您的第一个问题(类似地命名为包含文件)。这很丑陋,而且大多数人讨厌它(所以我经常被迫不在工作中使用它),但是你的经验表明这个想法有一些价值 - 即使它很丑陋(但是整个包括后卫的东西是一种黑客 - 为什么不全力以赴?):
#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中。但我认为 - 传统,我想。
答案 2 :(得分:14)
名称空间和模板不应跨越标题。亲爱的,不要告诉我你这样做:
template <typename foo>
class bar {
#include "bar_impl.h"
};
你已经说过了。
答案 3 :(得分:9)
正如已经指出的那样,C ++标准应该考虑到不同的开发平台,其中一些可能有限制,一旦支持不可能实现#pragma。
另一方面,由于之前类似的原因,没有添加对线程的支持,但是较新的C ++ Standard包含了线程。在后一种情况下,我们可以针对一个非常有限的平台进行交叉编译,但是开发是在一个成熟的平台上完成的。由于GCC支持此扩展,我认为,您的问题的真正答案是没有兴趣的人将此功能推入C ++标准。
从实际的角度来看,包括守卫在内,比起#pragma once指令的不合规使我们的团队更加麻烦。例如,如果文件重复并且稍后包含两个副本,则包含保护中的GUID不起作用。只使用#pragma一旦我们得到重复的定义错误并且可以花时间统一源代码。但是在包含防护装置的情况下,问题可能需要运行时测试来捕获,例如如果副本在函数参数的默认参数上不同,则会发生这种情况。
我避免使用包含警卫。如果我必须在没有#pragma支持的情况下将代码移植到编译器,我将编写一个脚本,它将向所有头文件添加包含防护。
答案 4 :(得分: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
答案 5 :(得分:3)
IIRC,#pragma 任何都不是该语言的一部分。它在实践中显示出很多。
(编辑:完全同意包含和链接系统应该更多地成为新标准的焦点,因为它是最明显的弱点之一,在'今天这个时代')
答案 6 :(得分:2)
通过将include guard设置为包含文件中类和命名空间的名称,可以避免名称冲突,而无需使用随机字符串。
此外,MS编译器和GCC都支持#pragma once一段时间了,所以为什么它不符合ISO标准呢?
答案 7 :(得分:2)
务实的解决方案:
1)选择一些一致的保护命名策略(例如,相对于项目根目录的路径+文件名,或者您选择的其他任何内容)。包括第三方代码的可能例外。
2)编写一个程序(一个简单的python脚本会这样做)以递归方式遍历源树并验证守卫是否与策略保持一致。每当警卫出错时,输出一个用户可以轻松应用来修复的diff(或sed脚本或其他任何内容)。或者只是要求确认并从同一程序进行更改。
3)让项目中的每个人都使用它(比如,在提交源代码管理之前)。
答案 8 :(得分:2)
我认为允许执行多个包含特殊编译指示的正确方法,默认情况下禁止多个包含 例如:
#pragma allow_multiple_include_this_file
所以既然你问为什么。您是否将您的建议发送给标准开发人员? :) 我也不发送。 一个人可以成为理由吗?