是否依赖于 GCC's/LLVM 的 `-fexceptions` 技术上未定义的行为?

Is relying on GCC's/LLVM's `-fexceptions` technically undefined behavior?

据我所知,可以考虑编译器扩展 undefined rather than implementation-defined。我猜测(但不确定)这适用于 C++ 标准和 C 标准。

GCC 和 LLVM 都提供了一个 -fexceptions 功能,似乎可以确保从 C++ 代码 通过 C 代码 抛出异常,然后在 C++ 代码中捕获它会正常运行正如预期的那样,即在 C 和 C++ 中展开堆栈帧并为 C++ 本地调用析构函数。 (注意:我知道在展开的 C 堆栈帧中分配的资源不会被释放。这不是我的问题的一部分。)这是来自 GCC documentation:

的相关文本

If you do not specify this option, GCC enables it by default for languages like C++ that normally require exception handling, and disables it for languages like C that do not normally require it. However, you may need to enable this option when compiling C code that needs to interoperate properly with exception handlers written in C++.

但是,我在 C 或 C++ 标准中找不到任何内容表明堆栈展开应该如何与包含从不同源语言编译的帧的堆栈交互。 C++标准似乎只在15.2中提到了unwinding,except.ctor,它只是简单地解释了抛出异常时销毁本地对象的规则。

因此,是否通过 C 代码未定义行为传递异常,即使使用旨在使其以明确定义的方式工作的语言扩展?正在使用这样一个实现提供的扩展 "wrong"?

对于上下文,这个问题的灵感来自 Rust 社区中关于通过 C 代码展开堆栈的两个相当冗长的讨论:

从某种意义上说,C 没有定义当您调用用 C 以外的语言编写的函数时会发生什么,更不用说如果该函数失败 return 而是结束其生命周期 和 C 调用者的生命周期,是的,它是未定义的行为。它是 not "implementation-defined behavior",因为实现定义行为的定义特征是语言标准对实现记录特定行为的实现施加了要求,而这不是案例在这里;所讨论的主题完全超出了相关标准的范围。

从合理和可移植的 C 编程的角度来看,您不应该使用或依赖 -fexceptions 并且旨在从 C 调用的 C++ 代码应该捕获最外层 extern "C" 函数中的所有异常(或通过指向 C 调用者的函数指针公开的函数)并将它们转换为错误代码或与 C 兼容的某种机制(例如 longjmp,但前提是已记录 C 调用者必须为被调用者做好准备这样做)。

依赖实施文档

这里的基本问题是我们是否可以依赖 C 或 C++ 实现提供的规范。 (由于我们正在处理混合 C 和 C++ 代码的情况,因此我将这种组合实现称为单一实现。)

其实还是要靠实现文档。 C 和 C++ 标准不适用,除非并且直到实现断言它符合(至少部分)标准。标准没有法律效力;在有人决定采用它们之前,它们不适用于任何人或企业。 (C 2018 前言提到 ISO statement 解释标准是自愿的。)

如果一个实现告诉您它符合 C 和 C++ 标准,并且它还告诉您它支持通过 C 代码抛出 C++ 异常,那么没有理由相信一个而不相信另一个。如果您接受实现的文档,那么它既符合语言标准,又支持通过 C 代码抛出异常。如果您不接受实现的文档,那么就没有理由期望符合语言标准。 (这是一般观点,忽略了明显的错误让我们有理由怀疑特定行为的情况,例如。)

如果您问通过 C 代码传递异常是否是 C 或 C++ 标准中使用的“未定义”,答案是肯定的。但这些标准 讨论 它们 定义的内容。他们使用“未定义”并不禁止其他任何人定义行为。事实上,如果您使用的是实现的文档,那么您就有了行为的定义。 C 和 C++ 标准不会撤消、否定或取消其他文档所做的定义:

  • C 或 C++ 标准指出任何行为未定义,这仅意味着该行为在 C 或 C++ 标准的上下文中未定义。
  • 程序员选择使用的任何其他规范都可以定义 C 或 C++ 标准未定义的其他行为。 C 和 C++ 标准不禁止这样做。

例子

例如,一些可能依赖于指定商业软件产品行为的文档包括:

  • C 标准。
  • C++ 标准。
  • 汇编手册。
  • 编译器文档。
  • Apple 的开发人员工具文档,包括 Xcode、链接器和软件构建期间使用的其他工具的行为。
  • 处理器手册。
  • 指令集架构规范。
  • IEEE-754 浮点运算标准。
  • 命令行工具的 Unix 文档。
  • 系统接口的 Unix 文档。

对于很多软件来说,如果整体行为不是由所有这些规范组合定义的,就不可能生产软件。 C 或 C++ 标准覆盖或胜过其他文档的想法是荒谬的。

编写可移植代码

任何软件项目或任何工程项目都在前提下工作:它采用给定的各种工具规格、material 属性、设备属性等,并从这些前提中导出所需的产品。很少有任何完整的最终用户商业产品仅依赖于 C 或 C++ 标准。当您购买 iPhone 时,它遵守物理定律,您有权相信它符合电子设备的安全规范和政府机构规定的无线电频率行为。它符合许多规范,认为 C 标准应该凌驾于其他规范之上的想法是荒谬的。如果你的设备因为 C 标准所说的具有未定义行为的编程错误而起火,那是不可接受的——C 标准说它未定义的事实并不能超越安全规范。

即使在纯软件项目中,也很少有严格遵守C或C++标准的。在很大程度上,只有进行一些纯计算和有限 input/output 的软件才能用严格符合 C 或 C++ 的语言编写。这可能包括其他软件中包含的非常有用的库,但它包括很少的完整商业最终用户程序——例如数学家和科学家用来回答有关逻辑、数学和建模问题的一些东西。这个世界上的大多数软件都以 C 或 C++ 标准未定义的方式与设备和操作系统交互。大多数软件使用标准未定义的扩展——这些扩展以标准未定义的方式操作文件和内存,以标准未定义的方式与设备和用户交互。它们显示 GUI windows 并接受用户的鼠标和键盘输入。它们通过网络传输和接收数据。他们向其他设备发送无线电波。

如果不使用语言标准未定义的行为,这些事情是不可能的。而且,如果语言标准超越了这些行为的定义,那么编写这样的软件将是不可能的。如果你想发送 Wi-Fi 无线电信号,并且你采用了 C 标准,并且 C 标准胜过其他定义,这意味着你不可能编写可靠发送无线电信号的软件。显然,事实并非如此。 C 标准并不优于其他规范。

对于大多数软件项目来说,编写“可移植代码”并不是一个可行的要求。当然,包含不可移植的代码以清除接口是可取的。希望使用可移植代码编写可以重用的代码。但这只是大多数项目的一部分。对于大多数项目,项目作为一个整体必须使用文档定义的行为而不是语言标准。

代码不是 UB,因为代码不是 C++ 语言,代码是 C++ 和 gcc/clang扩展语言。在具有 gcc/clang 扩展的 C++ 中,代码已记录且定义明确。在 C++ 中,相同的代码将是 UB。

因此,如果您采用相同的代码并使用纯标准 C++ 对其进行编译,那么该代码将显示 UB。但是,如果您使用 gcc/clang 扩展名在 C++ 中编译它,那么代码定义明确。