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

问题描述

据我所知,在编译编译单元时,编译器的预处理器通过扩展在 #include 和 {{ 之间指定的头文件1内容来翻译 < 指令1}}(或 >标记进入当前编译单元。

我的理解是,大多数编译器都支持 " 指令,以防止由于多次包含同一标头而导致多个定义的符号。遵循 include 守卫习语可以产生相同的效果

我的问题有两个:

  1. 如果编译器之前遇到过 #pragma once 指令或在此标头中包含保护模式,则完全忽略 #include 指令是否合法?
  2. 特别是与 Microsoft 的编译器相比,标头是否包含 #pragma once 指令或包含保护模式在这方面有什么区别吗? documentation 表明它们的处理方式相同,尽管有些用户 feels very strongly that I am wrong,所以我很困惑,想澄清一下。

1我掩盖了一个事实,即headers need not necessarily be files

解决方法

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

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

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

,

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

标准说 pragma 指令导致实现定义的不符合结果是可以的:

Pragma 指令 [cpp.pragma] ... 导致实现以实现定义的方式运行。该行为可能会导致 翻译失败或导致翻译器或生成的程序以不符合规定的方式运行。 任何未被实现识别的编译指示都会被忽略。

关于 MSVC 行为,您可以认为它会根据标准化路径跳过标头。例如,您可以使用符号链接欺骗编译器:

test/test.h

#pragma once

static int x = 2;

创建符号链接“test-link”到“test”目录:

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"

原因

错误 C2374:'x':重新定义;多次初始化

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

,
  1. 如果编译器之前遇到过 #pragma once 指令或在此标头中包含保护模式,则完全忽略 #include 指令是否合法?

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

但是,我所知道的所有支持 #pramga once 的编译器都将其视为包裹整个文件的非可变唯一包含保护。

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

因此,在这方面,如果他们没有犯实现错误,那么忽略包含是安全的。

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

  1. 特别是对于 Microsoft 的编译器,头文件是否包含 #pragma once 指令或包含保护模式在这方面有什么区别吗?文档表明它们的处理方式相同,尽管有些用户强烈认为我错了,所以我很困惑并希望得到澄清。

如果没有使用 #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 是否被另一个文件调用。

因此,在这种情况下,它只能忽略文件的内容,前提是它可以确保所有相关定义都相同和/或宏的评估结果为空文件。

,

如果编译器之前遇到过 BitBlt 指令或在此标头中包含保护模式,则完全忽略 #include 指令是否合法?

当然可以!编译器忽略所有源文件和头文件甚至是合法的,只要生成的代码的行为与就好像一样,它处理了所有内容。这就是预编译头文件和目标文件的工作方式——任何没有改变的东西都可以安全地忽略。同样,如果编译器可以证明包含和不包含文件将具有完全相同的行为,则编译器可能会忽略该文件,而不管预处理器指令如何。

特别是与 Microsoft 的编译器在这方面是否有任何区别,标头是否包含 #pragma once 指令或包含保护模式?

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

,

#pragma once 显然是指整个文件

#pragma once 的使用可以减少构建时间,因为编译器不会 在文件的第一个#include 之后再次打开并读取文件 翻译单元。

真的 - 如果不提​​交 - 它可以关联什么?

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

让我们做一些测试。这里也将是非常有用的 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 次。

因此,如果在第一个条件块之外存在任何非空(注释、空白字符序列(空格、制表符、换行符))语句 - 文件已包含更多次。

当然可以,下一步

// 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 与文件关联,然后只要它不是未定义的 - 文件将不会被更多地包含。这是特定编译器的隐式优化。并且非常脆弱。任何非空格或注释语句都会破坏它。尽管有记录表明 #ifndef HEADER_H_ 等同于 #if !defined HEADER_H_ - 事实上这不是真的。