为什么C++中没有默认的include guards机制?

10

我在我的C++项目中的每个头文件中使用#pragma once(或者您使用像#ifndef...这样的包含保护),这是巧合还是大多数开源项目中常见的做法(为避免仅基于个人项目经验的答案)。如果是这样,请问为什么不反过来:如果我想要一个头文件被多次包含,我使用一些特殊的预处理器命令,如果不是,我则将文件保持原样。


9
#pragma once 不是标准写法;而 #ifndef/#define/#endif 则是标准写法。 - T.C.
3
@ T.C. 好的。但问题基本上保持不变。 - NOhs
4
可能有一些奇怪的历史原因,与逻辑无关。 - Étienne
4
现在更改它将会是一个兼容性的噩梦。正确地指定也可能很困难。(什么算作“相同的文件”?在不区分大小写的系统中,“A.h”和“a.h”是否相同?硬链接或符号链接呢?) - T.C.
3
好的,我会尽力进行翻译。以下是需要翻译的内容:相关链接:https://dev59.com/CnI-5IYBdhLWcg3wwLS3为什么C++的#pragma once不是ISO标准? - T.C.
显示剩余4条评论
3个回答

9
由于历史原因,C++预处理器与40年前设计的C预处理器几乎相同。那时的编译器要简单得多。预处理器更简单,只是一个愚蠢的宏处理器,甚至不是编译器。尽管C++标准未指定标准头文件的工作方式,但在概念上,“#include”仍然与40多年前相同:它会导致预处理器将命名文件的内容插入到包含文件中。
在没有大量相互依赖和子模块的简单1970年代C代码库中,可能不需要包含保护。早期的非标准C没有使用函数原型,这就是现在大多数包含文件所用的东西。如果两次包含头文件会导致错误,您可能可以重新排列代码以防止其被重复包含。
随着代码库的增长和变得更加复杂,我认为意外包含头文件两次(可能通过其他头文件间接包含)的问题变得更加普遍。一种解决方法是改变预处理器使其更智能,但这需要每个人都使用新的预处理器,并使其更大、更慢。因此,发展出了一种约定,使用预处理器的现有功能(“#define”和“#ifndef”),而不是防止头文件被重复包含,只是使头文件重复包含变得无害,因为在第一次包含后它没有任何效果。
随着时间的推移,这种约定变得越来越普遍,现在几乎是通用的,除了那些旨在被包含两次并编写为正确工作的头文件的罕见示例(例如“”)。
后来,一些编译器引入了“#pragma once”作为另一种非便携式的方式来具有与包含保护相同的效果,但当时世界各地使用了许多不同的C预处理器副本,而包含保护已成为惯例。
因此,当前行为几乎肯定是由于历史原因。如果从头开始设计现代计算机编写的现代语言不会使用类似于C预处理器的东西。但是C并不是21世纪设计的。我认为包含保护约定是随着时间缓慢建立的,并且不需要对现有软件进行任何更改即可使其正常工作。现在更改它将破坏依赖于当前行为的未知数量的C和C ++代码,这可能不是一个选项,因为向后兼容性对于C和C ++都很重要。

8
C++编译器的行为是根据它如何处理每个翻译单元来指定的。翻译单元是经过预处理器处理后的单个文件。事实上,我们有一个将声明收集在某些文件中并称之为“头”文件的约定对于编译器或C++标准来说毫无意义。
简单地说,标准不提供“头文件”,因此不能提供自动包含保护头文件。标准仅提供预处理器指令#include,其余仅仅是惯例。除了可怜的维护该代码的人之外,没有任何东西阻止您前置声明所有内容并且不使用头文件(...)。
因此,头文件并不特殊,也没有办法说“那是头文件,请保护它”,但为什么不能保护所有被#include的文件呢?因为#include比其他语言具有模块系统更加强大和不足。#include会导致预处理器粘贴其他文件,而不一定是头文件。如果您在不同的文件中拥有相同的命名空间、typedef声明,这样做可能很方便。您可以将它们收集到一个文件中,并在几个位置#include它们。您不希望自动包含保护阻止您这样做。
使用#ifndef和#define有条件地包含头文件也仅仅是惯例。标准没有“包含保护”的概念。(然而,现代编译器实际上知道包含保护。识别包含保护可以加快编译速度,但与正确实现标准无关。)
严格来说,标准确实在很多情况下使用“头文件”这个词,特别是指C和C++标准库。然而,#include的行为在“§ 16.2 *Source file* inclusion”(我强调)下定义,并且它不赋予头文件任何特殊功能。
目前有努力将一个适当的模块系统引入C++标准。

1
有“头文件”和“源文件”。 “头文件”不一定需要是实际的文件。 - T.C.
真的吗?请说说看。这是因为标准允许使用<>中的内容由任何实现定义的方式提供吗? - Praxeolitic
4
正确的做法(或未指定,而不是实现定义)。唯一需要做的就是写 #include <vector> 会导致适当的声明和定义被看到。编译器可以有特殊处理来识别标准头文件名并将其内容硬编码到其中,或者它可以从对象数据库获取AST数据块...或者它可以只是让预处理器在文本上包含一个文件(这是所有常见实现实际上所做的)。 - Jonathan Wakely
@JonathanWakely 在我还没来得及完整提出问题之前就已经到这里了,但我发现这很有趣,仍然觉得值得分享。https://dev59.com/W14d5IYBdhLWcg3wCOyD - Praxeolitic
我补充一下,有大量的代码包含了同一个头文件但没有使用守卫。这通常是通过使用宏定义来定义初始化数据结构。与 Bliss或汇编语言相比,C/C++预处理器一直都很原始。 - user3344003

0
我必须同意,主要因素是历史原因,尽管如此,有时你会看到依赖于它们不存在的代码。MAME就是一个例子:它通过多次包含具有不同宏定义的可读宏文件来构建复杂的数据结构(或者至少在我上次查看时是这样)。如果包含保护是自动的,你会遇到需要关闭它们的代码。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接