编译器忽略#include 指令是否合法?

Is it legal for a compiler to ignore an #include directive?

据我了解,在编译编译单元时,编译器的预处理器通过扩展 header 文件 1 之间指定的内容来翻译 #include 指令<>(或 ")标记到当前编译单元。

据我了解,大多数编译器都支持 #pragma once 指令,以防止由于多次包含相同的 header 而导致多重定义的符号。遵循 include guard 习语可以产生相同的效果。

我的问题是two-fold:

  1. 如果编译器之前遇到过 #pragma once 指令,那么完全忽略 #include 指令是否合法 或者在此 中包含保护模式 =46=]?
  2. 特别是对于 Microsoft 的编译器,header 是否包含 #pragma once 指令 或 include guard 模式在这方面有什么区别吗? documentation suggests that they are handled the same, though some user ,所以我很困惑,希望得到澄清。

1 我完全是在掩盖 headers need not necessarily be files 的事实。

编译后的程序无法判断编译器是否忽略了头文件,在as-if rule下忽略或不忽略都是合法的。

如果忽略文件导致程序具有与正常处理所有文件所产生的程序不同的可观察行为,忽略文件会导致程序在处理时无效通常不会,那么忽略此类文件是不合法的。这样做是一个编译器错误。

编译器编写者似乎确信忽略具有适当包含防护的曾经见过的文件不会对生成的程序产生影响,否则编译器不会进行此优化。有可能他们都错了,并且有一个迄今为止还没有人找到的反例。也有可能不存在这样的反例是一个没有人费心去证明的定理,因为它在直觉上似乎是显而易见的。

我认为您可以将 #pragma once 视为编译器语言扩展,例如 #pragma omp parallel 可以使循环并行执行,如果编写不正确会导致各种 UB。

标准说 pragma 指令可以导致 implementation-defined non-conforming 结果:

Pragma directive [cpp.pragma] ... causes the implementation to behave in an implementation-defined manner. The behavior might cause translation to fail or cause the translator or the resulting program to behave in a non-conforming manner. Any pragma that is not recognized by the implementation is ignored.

关于 MSVC 行为,您可以认为它基于其规范化 path.For 实例跳过 header,您可以使用符号链接欺骗编译器:

test/test.h

#pragma once

static int x = 2;

创建指向“测试”目录的符号链接“test-link”:

mklink /d test-link test

然后在 main.cpp:

#include "test/test.h"
#include "test/test.h"
#include "test/../test/test.h"

没问题。但是

#include "test/test.h"
#include "test-link/test.h"

原因

error C2374: 'x': redefinition; multiple initialization

在包含守卫的情况下不会发生这种情况。

  1. Is it legal for a compiler to completely ignore an #include directive if it has previously encountered a #pragma once directive or include guard pattern in this header?

这取决于编译器如何定义和实现 #pramga once。毕竟这是一个 none 标准功能。

但是,据我所知,所有支持 #pramga once 的编译器都将其视为一个不可变的唯一包含保护程序,它环绕着整个文件。

预处理器解析包含路径后,它可以检查该文件是否已包含以及该文件是否存在 #pargma once。如果两个条件都为真,则不再包含该文件是安全的,因为它会遵循 as-if rule,因为编译器供应商完全控制 #pramga once 的实现方式,并且可以确保lock guard 是唯一的,不可改变的,并且包装了整个文件,因此重复包含相同的 wile 会导致包含空内容。

因此,在这方面,如果他们没有犯实施错误,则可以安全地忽略包含。

有反对使用 #pragma once 的论点,认为由于符号链接和硬链接,编译器可能将同一文件视为不同文件。这会导致意外地多次包含同一个文件,但是如果编译将其识别为同一个文件,这不会影响忽略它是否安全的部分。

  1. Specifically with Microsoft' compiler is there any difference in this regard whether a header contains a #pragma once directive or an include guard pattern? The documentation suggests that they are handled the same, though some user feels very strongly that I am wrong, so I am confused and want clarification.

如果不使用#pragma once,它会变得更复杂。预处理器需要首先检查锁守卫是否环绕所有内容:

#ifndef SOME_GUARD_NAME_H
#define SOME_GUARD_NAME_H
// all content of the file
#endif

或者如果是这样的话:

// some content before the guard
#ifndef SOME_GUARD_NAME_H
#define SOME_GUARD_NAME_H
// some content
#else
// some more content
#endif
// some other content after the guard

并且它需要跟踪 SOME_GUARD_NAME_H 是否已经在另一个文件中定义或者 #undef 是否被另一个文件调用。

所以在那种情况下,如果它可以确保所有相关定义都相同,那么它只能忽略文件的内容and/or如果宏的计算结果是一个空文件。

#pragma once显然是指整个文件

The use of #pragma once can reduce build times, as the compiler won't open and read the file again after the first #include of the file in the translation unit.

真的 - 如果不归档 - 它可能与什么有关?

条件编译,所谓的 guard idiom 与文件无关,但与代码块有关。真的 - 在哪里,如何说明某些条件 与文件 相关?! 它与 block 相关,以 #if* 开始并以 #endif 结束。编译器无论如何都需要再次包含这个文件。

让我们做一些测试。这里也将是非常有用的 cl(msvc) 编译器选项 /showIncludes

让我们创建header.h

// header.h
#ifndef HEADER_H_ 
#define HEADER_H_
int g_a = 0;
#endif 

然后

#include "header.h"
#include "header.h"

日志中只有一次

1>Note: including file: .\header.h

所以header.h这里真的只收录了一次

但如果这样做

// header.h 
#if !defined HEADER_H_
#define HEADER_H_
int g_a = 0;
#endif 

或这个

#if !defined(HEADER_H_)
#define HEADER_H_
int g_a = 0;
#endif 

#include "header.h"
#include "header.h"

日志中已有 2 行 - header.h 包含 2 次。

1>Note: including file: .\header.h
1>Note: including file: .\header.h

所以#ifndef HEADER_H_有不同的效果比较#if !defined(HEADER_H_)

或者如果

// header.h
#ifndef HEADER_H_ 
#define HEADER_H_
int g_a = 0;
#endif 
#define XYZ

// header.h
#if __LINE__ // any not empty statement
#endif

#ifndef HEADER_H_ 
#define HEADER_H_
int g_a = 0;
#endif 

#include "header.h"
#include "header.h"

已经

1>Note: including file: .\header.h
1>Note: including file: .\header.h

日志中再次出现 2 行 - header.h 包含 2 次。

所以如果在第一个条件块之外存在任何非空的(注释,白色space字符序列(space,制表符,换行符))语句 - 文件已经包含了更多次。

当然可以,下一步

// header.h
#include "header.h"
#undef HEADER_H_
#include "header.h"

在这种情况下

1>Note: including file: .\header.h
1>Note: including file: .\header.h
1>.\header.h(4): error C2374: 'g_a': redefinition; multiple initialization
1>.\header.h(4): note: see declaration of g_a'

当然还有以防万一

// header.h 
#pragma once
int g_b = 0;

#include "header.h"
#include "header.h"

只有单行

1>Note: including file: .\header.h

所以根据测试可以得出下一个结论 - 如果 cl(msvc) - 查看该文件有模式

#ifndef macro // but not #if !defined macro
#define macro
// all code only here
#endif

macro 与文件相关联,然后只要它不是未定义 - 文件将不会被包含更多。这是特定编译器的隐式优化。并且非常脆弱。任何非白色 space 或评论语句都会破坏它。即使记录在案 #ifndef HEADER_H_ 等价于 #if !defined HEADER_H_ - 事实上这不是真的。

Is it legal for a compiler to completely ignore an #include directive if it has previously encountered a #pragma once directive or include guard pattern in this header?

当然是!编译器忽略所有源文件和 header 文件甚至是合法的,只要生成的代码的行为与 就好像 它处理所有内容一样。这就是 pre-compiled headers 和 object 文件的工作方式 - 任何未更改的内容都可以安全地忽略。同样,如果编译器可以证明包含和不包含文件将具有完全相同的行为,编译器可能会忽略该文件,而不管 pre-processor 指令如何。

Specifically with Microsoft' compiler is there any difference in this regard whether a header contains a #pragma once directive or an include guard pattern?

文档对此非常清楚。它们是相同的,假设编译器设法识别成语并且您没有 #undefed 宏。我也从未遇到过与此相关的任何错误。 #pragma once 虽然更安全。我有一个实例,其中两个 headers 具有相同的包含保护和调试,这不是一个很好的体验。