为什么一次不自动假定#pragma?


81

告诉编译器只包含一次文件有什么意义?默认情况下是否有意义?甚至没有理由多次包含一个文件吗?为什么不只是假设呢?与特定的硬件有关吗?


24
甚至没有理由多次包含一个文件吗?=>可以。文件中可能有条件编译#ifdefs。所以,你可能会说#define MODE_ONE 1,然后#include "has-modes.h",然后#undef MODE_ONE#define MODE_TWO 1#include "has-modes.h"一次。预处理器对这类事情是不可知的,也许有时它们是有意义的。
HostileFork说不要信任

66
将其作为默认设置是有意义的。只是没有一个他们选择当C程序员仍然骑马,携带枪支和有内存16KB
汉斯帕桑特

11
您可以在同一源文件中<assert.h>多次包含不同的定义NDEBUG
皮特·贝克尔

3
#pragma once其本身而言,在某些硬件环境(通常是具有网络驱动器,并且可能有多个路径指向同一标头)的地方,它们将无法正常工作。
皮特·贝克尔

12
如果您已#pragma once假定,应对该默认设置的方式是什么? #pragma many?有多少编译器实现了这样的功能?
乔纳森·勒夫勒

Answers:


84

这里有多个相关问题:

  • 为什么#pragma once不自动执行?
    因为在某些情况下您想多次包含文件。

  • 为什么要多次包含文件?
    其他答案(Boost.Preprocessor,X-Macros,包括数据文件)中给出了几个原因。我想添加一个“避免代码重复”的特定示例:OpenFOAM鼓励一种#include在函数中包含点点滴滴是一种常见概念的样式。例如,请参阅讨论。

  • 好的,但是为什么不选择退出默认设置?
    因为它不是标准实际指定的。#pragma根据定义,s是特定于实现的扩展。

  • 为什么还#pragma once没有成为标准化功能(得到广泛支持)?
    因为以与平台无关的方式固定“相同文件”实际上是非常困难的。有关更多信息,请参见此答案



4
特别是,在发生故障但包含防护将起作用的情况下,请参见此示例pragma once。通过位置识别文件也不起作用,因为有时同一文件在项目中会多次出现(例如,您有2个子模块,两个子模块的标头中都包含仅标头的库,并签出自己的副本)
MM

6
并非所有实用程序都是特定于实现的扩展。例如#pragma STDC家庭。但是它们都可以控制实现定义的行为。
罗斯兰

3
@ user4581301这个答案一次过地夸大了实用性问题,并且不考虑由于包括防护措施而引起的麻烦。在两种情况下都需要一些纪律。使用include防护时,必须确保使用不会在其他文件中使用的名称(在文件副本修改后会发生)。一旦有了编译指示,就必须决定文件的唯一正确位置,这毕竟是一件好事。
奥利夫,

3
@Mehrdad:您是否认真建议编译器写入源文件!?如果编译器看到#ifndef XX,则在#endif读取整个文件之前,它必须不知道在对应的内容之后是否还有任何内容。该保持的最外层是否跟踪编译器#ifndef包含整个文件和笔记什么的宏它检查可能能够避免再次扫描文件,而是一个指令,没有什么遵循目前的点似乎很重要的似乎更好比在编译器依赖于记住这样的事情。
超级猫

38

您可以在文件中的#include 任何地方使用,而不仅仅是在全局范围内使用,例如在函数内部使用(如果需要,可以多次使用)。当然,丑陋而不是好的样式,但是可能并且有时是明智的(取决于所包含的文件)。如果#include只是一次性的事情,那将是行不通的。#include毕竟只是做愚蠢的文本替换(剪切'n'粘贴),并不是您包括的所有内容都必须是头文件。例如,您可能会#include包含一个包含自动生成的数据的文件,该文件包含用于初始化的原始数据std::vector。喜欢

std::vector<int> data = {
#include "my_generated_data.txt"
}

并让“ my_generation_data.txt”成为编译过程中由构建系统生成的东西。

或者,也许我懒惰/愚蠢/愚蠢,并将其放在文件中(非常人为的示例):

const noexcept;

然后我做

class foo {
    void f1()
    #include "stupid.file"
    int f2(int)
    #include "stupid.file"
};

另一个人为设计较少的示例是源文件,其中许多函数需要在命名空间中使用大量类型,但是您不想只说using namespace foo;全局,因为这会污染很多其他东西你不要 因此,您创建了一个包含以下内容的文件“ foo”

using std::vector;
using std::array;
using std::rotate;
... You get the idea ...

然后在源文件中执行此操作

void f1() {
#include "foo" // needs "stuff"
}

void f2() {
    // Doesn't need "stuff"
}

void f3() {
#include "foo" // also needs "stuff"
}

注意:我不主张这样做。但是在某些代码库中这是可能的并且已经完成了,我不明白为什么不应该这样做。它确实有其用途。

也可能是您包含的文件的行为取决于某些宏(#defines)的值。因此,您可能需要在更改一些值之后将文件包含在多个位置,以便在源文件的不同部分获得不同的行为。


1
如果所有标头都为一次编译指示,则此操作仍然有效。只要您不只一次包含生成的数据。
PSkocik

2
@PSkocik但也许我需要多次添加它。我为什么不能呢?
Jesper Juhl

2
@JesperJuhl这就是重点。您不需要一次包含它。您目前可以选择,但替代方法也不会差很多。
Johnny Cache

9
@PSkocik如果我#define在每个include更改s的值之前更改了s的值,那么s的值会更改包含文件的行为,那么我可能非常需要多次包含它,才能在源文件的不同部分获得不同的行为。
Jesper Juhl '18

27

可以多次使用,例如,使用X宏技术:

data.inc:

X(ONE)
X(TWO)
X(THREE)

use_data_inc_twice.c

enum data_e { 
#define X(V) V,
   #include "data.inc"
#undef X
};
char const* data_e__strings[]={
#define X(V) [V]=#V,
   #include "data.inc"
#undef X
};

我对其他用途一无所知。


听起来太复杂了。是否有理由不首先将这些定义包括在文件中?
Johnny Cache

2
@JohnnyCache:该示例是X-宏的工作方式的简化版本。请阅读链接;在某些情况下,它们对于表格数据的复杂操作非常有用。在X宏的任何重要用法中,都无法仅将“这些定义包括在文件中”。
Nicol Bolas

2
@Johnny-是的-一个很好的理由是要确保一致性(当您只有几十个元素而不必担心数百个元素时,手工很难做到)。
Toby Speight

@TobySpeight嘿,我想我可以保留一行代码,以避免在其他地方编写成千上万个代码。说得通。
Johnny Cache

1
避免重复。特别是如果文件很大。诚然,您可以只使用包含X宏列表的大宏,但是由于项目可能正在使用此宏,因此强制#pragma once行为将是一项重大更改。
PSkocik

21

您似乎在假设即使语言中也存在“ #include”功能的目的是为将程序分解为多个编译单元提供支持。那是不对的。

它可以执行该角色,但这不是其预期目的。C最初开发为比PDP-11 Macro-11汇编程序稍微高级的语言,用于重新实现Unix。它具有宏预处理器,因为这是Macro-11的功能。它具有从文本上包含另一个文件中的宏的功能,因为这是Macro-11的功能,Macro-11的功能是将它们移植到新的C编译器中并已使用。

现在事实证明,“#include”用于将代码分离为编译单元,这(有点可笑)很有用。然而,事实证明这个hack存在意味着它成为了在C.做到这一点的方式存在意味着没有新的方法不断的事实之路需要要创建提供此功能,所以没有什么更安全(如:不容易受到多-inclusion)。由于它已经在C语言中,因此它与C语言的其余大部分语法和惯用法一起被复制到C ++中。

有建议为C ++提供适当的模块系统,以便最终可以消除这个已有45年历史的预处理程序。我不知道这有多迫切。十多年来,我一直在听说它的存在。


5
和往常一样,要了解C和C ++,您需要了解它们的历史。
杰克·艾德利

可以合理预期模块将在二月份登陆。
戴维斯·鲱鱼

7
@DavisHerring-是的,但是哪个二月?
TED

10

不,这会严重阻碍图书馆作家等可用的选项。例如,Boost.Preprocessor允许一个人使用预处理器循环,而实现这些循环的唯一方法是对同一文件进行多次包含。

Boost.Preprocessor是许多非常有用的库的构建块。


1
这不会阻碍任何事情。OP询问了默认行为,而不是不可更改的行为。更改默认值并提供预处理器标志#pragma reentrant或类似的东西是完全明智的。后见之明是20/20。
康拉德·鲁道夫'18

从强迫人们更新其库和依赖项(@KonradRudolph)的角度来看,这将是一个障碍。并非总是有问题,但是它可能会导致某些旧版程序出现问题。理想情况下,还会有一个命令行开关来指定默认值是once还是reentrant,以减轻此问题或其他潜在问题。
贾斯汀时间-恢复莫妮卡

1
@JustinTime正如我的评论所说,这显然不是向后兼容(因此可行)的更改。但是,问题是为什么它最初是以这种方式设计的,而不是为什么它没有被更改。明确的答案是,原始设计是一个巨大的错误,带来了深远的影响。
康拉德·鲁道夫

8

在我主要研究的产品的固件中,我们需要能够指定应在内存中分配函数和全局/静态变量的位置。实时处理需要驻留在芯片上的L1内存中,以便处理器可以直接,快速地访问它。不太重要的处理可以放在芯片的L2存储器中。不需要特别迅速处理的任何内容都可以存在于外部DDR中并进行缓存,因为速度稍慢一点都没有关系。

#pragma分配要去的地方很长,很重要。容易弄错。犯错的效果将是代码/数据将被悄悄放进默认(DDR)内存,并且效果可能的闭环控制停止无缘无故,很容易看到工作。

因此,我使用包含文件,其中仅包含该编译指示。我的代码现在看起来像这样。

头文件...

#ifndef HEADERFILE_H
#define HEADERFILE_H

#include "set_fast_storage.h"

/* Declare variables */

#include "set_slow_storage.h"

/* Declare functions for initialisation on startup */

#include "set_fast_storage.h"

/* Declare functions for real-time processing */

#include "set_storage_default.h"

#endif

来源...

#include "headerfile.h"

#include "set_fast_storage.h"

/* Define variables */

#include "set_slow_storage.h"

/* Define functions for initialisation on startup */

#include "set_fast_storage.h"

/* Define functions for real-time processing */

您会在此处注意到同一文件的多个包含,即使只是在标头中也是如此。如果我现在输入错误,编译器会告诉我找不到包含文件“ set_fat_storage.h”,我可以轻松地对其进行修复。

因此,在回答您的问题时,这是一个实际的,实际的示例,其中要求多个包含。


3
我想说您的用例是该_Pragma指令的激励示例。现在可以从常规宏扩展相同的编译指示。因此,无需多次包含。
StoryTeller-Unslander Monica,
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.