【问题标题】:Why isn't #pragma once automatically assumed?为什么不自动假定#pragma 一次?
【发布时间】:2019-04-24 07:02:39
【问题描述】:

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

【问题讨论】:

  • 是否有任何理由多次包含单个文件? => 可能。一个文件中可能有条件编译#ifdefs。所以你可能会说#define MODE_ONE 1,然后是#include "has-modes.h",然后是#undef MODE_ONE,又是#define MODE_TWO 1#include "has-modes.h"。预处理器对这些事情是不可知的,也许有时它们是有意义的。
  • 默认设置是有意义的。只是当 C 程序员仍然骑马、携带枪支并拥有 16KB 内存时,他们选择的不是那个
  • 你可以在同一个源文件中多次包含<assert.h>NDEBUG的定义不同。
  • 至于#pragma once 本身,在某些硬件环境(通常带有网络驱动器和可能通往同一标头的多个路径)中它无法正常工作。
  • 如果您假设#pragma once,那么取消该默认值的方法是什么? #pragma many?有多少编译器实现了这样的功能?

标签: c++ c pragma


【解决方案1】:

这里有多个相关问题:

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

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

  • 好的,但为什么它不是默认的选择退出?
    因为它实际上并没有被标准指定。 #pragmas 根据定义是特定于实现的扩展。

  • 为什么#pragma once 尚未成为标准化功能(因为它已得到广泛支持)?
    因为以与平台无关的方式确定什么是“相同的文件”实际上非常困难。 See this answer for more information.

【讨论】:

  • 特别是,see this example 用于 pragma once 失败但包含警卫的情况。按位置识别文件也不起作用,因为有时同一个文件会在一个项目中多次出现(例如,您有 2 个子模块,它们都在其标头中包含仅标头库并检查它们自己的副本)
  • 并非所有编译指示都是特定于实现的扩展。例如。 #pragma STDC family。但它们都控制实现定义的行为。
  • @user4581301 这个答案过度夸大了 pragma 的问题,并且没有考虑包含警卫的麻烦。在这两种情况下,都需要一些纪律。使用包含守卫,必须确保使用不会在其他文件中使用的名称(这将在文件复制修改后发生)。使用 pragma once,必须确定文件的唯一正确位置是什么,这毕竟是一件好事。
  • @Mehrdad:您是否认真建议编译器写入源文件!?如果编译器看到#ifndef XX,它必须在读取整个文件之前无法知道对应的#endif 后面是否有任何内容。跟踪最外层#ifndef 是否包含整个文件并记录它检查的宏可能能够避免重新扫描文件的编译器,但是对 say 的指令在当前之后没有什么重要的这似乎比依靠编译器来记住这些东西要好。
【解决方案2】:

您可以在文件中使用#include任何地方,而不仅仅是在全局范围内 - 比如在函数内部(如果需要,可以多次使用)。当然,丑陋和不好的风格,但可能而且偶尔是明智的(取决于您包含的文件)。如果#include 只是一次性的,那它就行不通了。 #include 毕竟只是进行了愚蠢的文本替换(cut'n'paste),并不是您包含的所有内容都必须是头文件。您可能 - 例如 - #include 包含自动生成的数据的文件,其中包含用于初始化 std::vector 的原始数据。喜欢

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

并且让“my_generated_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) 的值而异。因此,您可能希望在首先更改某些值之后将文件包含在多个位置,以便在源文件的不同部分获得不同的行为。

【讨论】:

  • 如果所有标题都是一次编译指示,这仍然有效。只要您没有多次包含生成的数据。
  • @PSkocik 但也许我需要不止一次地包含它。为什么我不能?
  • @JesperJuhl 这就是重点。您将不会需要不止一次地包含它。您目前可以选择,但如果有的话,替代方案也不会更糟。
  • @PSkocik 如果我在每个包含更改包含文件的行为之前更改#defines 的值,那么我很可能需要多次包含它以在不同部分获得这些不同的行为我的源文件。
【解决方案3】:

包含多次是可用的,例如,使用X-macro 技术:

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
};

我不知道还有什么其他用途。

【讨论】:

  • 这听起来过于复杂。有什么理由不首先将这些定义包含在文件中?
  • @JohnnyCache:该示例是 X-macros 工作原理的简化版本。请阅读链接;在某些情况下,它们对于对表格数据进行复杂操作非常有用。在 X 宏的任何重要使用中,您都无法“在文件中包含这些定义”。
  • @Johnny - 是的 - 一个很好的理由是确保一致性(当你只有几十个元素时很难手工完成,更不用说数百个了)。
  • @TobySpeight 嘿,我想我可以节省一行代码以避免在其他地方编写数千行代码。有道理。
  • 避免重复。特别是如果文件很大。诚然,您可以只使用包含 X 宏列表的大宏,但由于项目可能会使用它,因此强制 #pragma once 行为将是一个重大变化。
【解决方案4】:

您似乎在假设“#include”功能甚至存在于语言中的目的是为将程序分解为多个编译单元提供支持。 这是不正确的。

它可以扮演那个角色,但这不是它的预期目的。 C 是 originally developed 比 PDP-11 Macro-11 重新实现 Unix 的汇编语言稍高级。它有一个宏预处理器,因为那是 Macro-11 的一个特性。它能够以文本形式包含来自另一个文件的宏,因为这是 Macro-11 的一个特性,他们正在移植到新的 C 编译器的现有 Unix 已经利用了这一特性。

现在事实证明,“#include”有用可以将代码分成编译单元,这(可以说)有点像 hack。然而,这种 hack 存在的事实意味着它变成了用 C 完成的方式。方式存在的事实意味着永远需要创建新方法来提供此功能,因此没有什么更安全的了(例如:不易受到多重包含的影响)曾经被创建。由于它已经在 C 中,因此它与 C 的其他大部分语法和习语一起被复制到 C++ 中。

有一个提议给 C++ a proper module system 所以这个 45 岁的预处理器黑客终于可以省去了。我不知道这有多迫在眉睫。我听说它已经投入使用十多年了。

【讨论】:

  • 像往常一样,要了解 C 和 C++,您需要了解它们的历史。
  • 模块将在二月登陆reasonable to expect
  • @DavisHerring - 是的,但是哪个二月?
【解决方案5】:

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

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

【讨论】:

  • 它不会妨碍其中的任何。 OP 询问了 default 行为,而不是不可更改的行为。更改默认值并提供预处理器标志#pragma reentrant 或类似的东西是完全明智的。事后看来是 20/20。
  • 这会在强制人们更新他们的库和依赖项的意义上阻碍它,@KonradRudolph。并不总是一个问题,但它可能会导致一些遗留程序出现问题。理想情况下,还会有一个命令行开关来指定默认值是once 还是reentrant,以缓解此问题或其他潜在问题。
  • @JustinTime 正如我的评论所说,这显然不是向后兼容(因此是可行的)更改。然而,问题是为什么它最初是这样设计的,而不是为什么它没有被改变。毫无疑问,这个问题的答案是,最初的设计是一个具有深远影响的巨大错误。
【解决方案6】:

在我主要工作的产品的固件中,我们需要能够指定函数和全局/静态变量应该在内存中分配的位置。实时处理需要存在于 L1 片上内存中,因此处理器可以直接、快速地访问它。不太重要的处理可以在片上 L2 内存中进行。而任何不需要特别迅速处理的东西都可以存在于外部 DDR 中并通过缓存,因为它是否慢一点也没关系。

#pragma 分配事情的去向是一条很长的、不平凡的路线。很容易弄错。弄错的后果是代码/数据将被静默地放入默认 (DDR) 内存中,而 that 的后果可能是闭环控制无缘无故停止工作,这很容易见。

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

头文件...

#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”,我可以轻松修复它。

因此,在回答您的问题时,这是一个需要多重包含的真实、实用的示例。

【讨论】:

  • 我会说您的用例是_Pragma 指令的一个激励示例。现在可以从常规宏扩展相同的编译指示。所以不需要包含超过一次。
猜你喜欢
  • 1970-01-01
  • 2015-05-16
  • 2010-10-21
  • 1970-01-01
  • 2020-01-17
  • 2020-11-11
  • 2016-07-02
  • 1970-01-01
  • 2013-10-17
相关资源
最近更新 更多