ChatGPT解决这个技术问题 Extra ChatGPT

未定义、未指定和实现定义的行为

什么是 C 和 C++ 中的未定义行为 (UB)?未指定的行为和实现定义的行为呢?它们之间有什么区别?

我很确定我们以前做过这个,但我找不到。另请参阅:stackoverflow.com/questions/2301372/…
这里是 an interesting discussion(“附件 L 和未定义的行为”部分)。

B
Ben Voigt

未定义的行为是 C 和 C++ 语言的那些方面之一,可能会让来自其他语言的程序员感到惊讶(其他语言试图更好地隐藏它)。基本上,即使许多 C++ 编译器不会报告程序中的任何错误,也可以编写行为无法预测的 C++ 程序!

我们来看一个经典的例子:

#include <iostream>

int main()
{
    char* p = "hello!\n";   // yes I know, deprecated conversion
    p[0] = 'y';
    p[5] = 'w';
    std::cout << p;
}

变量 p 指向字符串文字 "hello!\n",下面的两个赋值尝试修改该字符串文字。这个程序有什么作用?根据 C++ 标准的第 2.14.5 节第 11 段,它调用 未定义的行为

尝试修改字符串文字的效果是未定义的。

我可以听到人们尖叫“但是等等,我可以编译这个没有问题并得到输出 yellow”或“你是什么意思未定义,字符串文字存储在只读内存中,所以第一次赋值尝试导致核心倾倒”。这正是未定义行为的问题。基本上,一旦您调用未定义的行为(甚至是鼻恶魔),该标准就允许任何事情发生。如果根据您的语言心理模型存在“正确”行为,那么该模型就是错误的; C++ 标准拥有唯一的投票权,句号。

未定义行为的其他示例包括访问数组超出其边界、dereferencing the null pointeraccessing objects after their lifetime ended 或将 allegedly clever expressions 写入 i++ + ++i

C++ 标准的第 1.9 节还提到了未定义行为的两个不太危险的兄弟,未指定行为和实现定义的行为:

本国际标准中的语义描述定义了一个参数化的非确定性抽象机。抽象机的某些方面和操作在本国际标准中被描述为实现定义(例如,sizeof(int))。这些构成了抽象机的参数。每个实现都应包括描述其在这些方面的特征和行为的文档。抽象机的某些其他方面和操作在本国际标准中描述为未指定(例如,函数参数的评估顺序)。在可能的情况下,本国际标准定义了一组允许的行为。这些定义了抽象机器的不确定性方面。本国际标准中将某些其他操作描述为未定义(例如,取消引用空指针的效果)。 [注:本国际标准对包含未定义行为的程序的行为没有要求。 ——尾注]

具体来说,第 1.3.24 节规定:

允许的未定义行为的范围从完全忽略具有不可预测结果的情况,到在翻译或程序执行期间以环境特征的记录方式表现(有或没有发出诊断消息),到终止翻译或执行(发出的诊断消息)。

您可以做些什么来避免遇到未定义的行为?基本上,您必须阅读知道他们在说什么的作者的good C++ books。避免使用互联网教程。避免公牛。


这是一个奇怪的事实,这是由于合并导致此答案仅涵盖 C++,但此问题的标签包括 C。C 具有不同的“未定义行为”概念:即使行为也被声明为,它仍然需要实现提供诊断消息对于某些规则违规(约束违规)未定义。
@Benoit这是未定义的行为,因为标准说它是未定义的行为,句号。在某些系统上,字符串文字确实存储在只读文本段中,如果您尝试修改字符串文字,程序将崩溃。在其他系统上,字符串文字确实会出现变化。该标准没有规定必须发生的事情。这就是未定义行为的含义。
@FredOverflow,为什么一个好的编译器允许我们编译给出未定义行为的代码?编译这种代码到底能带来什么好处?当我们试图编译给出未定义行为的代码时,为什么不是所有好的编译器都给我们一个巨大的红色警告标志?
@Pacerier 有些东西在编译时是不可检查的。例如,并不总是可以保证永远不会取消引用空指针,但这是未定义的。
@Celeritas,未定义的行为可以是不确定的。例如,不可能提前知道未初始化内存的内容是什么,例如。 int f(){int a; return a;}a 的值可能会在函数调用之间发生变化。
C
Community

好吧,这基本上是标准的直接复制粘贴

3.4.1 1 实现定义的行为 未指定的行为,其中每个实现都记录了如何做出选择 2 示例 实现定义的行为的一个例子是当有符号整数右移时高位的传播。 3.4.3 1 未定义行为 使用不可移植或错误程序结构或错误数据时的行为,本国际标准对此没有要求 2 注:可能的未定义行为范围从完全忽略具有不可预测结果的情况到在翻译或以环境特征的文件化方式执行程序(发出或不发出诊断消息),终止翻译或执行(发出诊断消息)。 3 示例 未定义行为的一个示例是整数溢出行为。 3.4.4 1 未指定的行为 使用未指定的值,或本国际标准提供两种或多种可能性且未强加任何进一步要求的其他行为 2 示例 未指定行为的示例是参数的顺序对函数进行评估。


实现定义的行为和未指定的行为有什么区别?
@Zolomon:就像它说的:基本上是一样的,除了在实现定义的情况下,实现需要记录(以保证)究竟会发生什么,而在未指定的情况下,不需要记录实现或保证什么。
@Zolomon:这反映在 3.4.1 和 2.4.4 之间的差异上。
@Celeritas:超现代编译器可以做得更好。给定 int foo(int x) { if (x >= 0) launch_missiles(); return x << 1; },编译器可以确定,由于所有不发射导弹的函数调用方式都会调用未定义行为,因此它可以无条件地调用 launch_missiles()
@northerner 正如引用所述,未指定的行为通常仅限于一组有限的可能行为。在某些情况下,您甚至可能得出结论,在给定的上下文中所有这些可能性都是可以接受的,在这种情况下,未指定的行为根本不是问题。未定义的行为完全不受限制(例如“程序可能决定格式化您的硬盘”)。未定义的行为总是一个问题。
j
justANewb stands with Ukraine

也许简单的措辞比标准的严格定义更容易理解。

实现定义的行为 语言说我们有数据类型。编译器供应商指定他们应该使用什么大小,并提供他们所做工作的文档。

未定义的行为
您做错了什么。例如,您在 int 中有一个非常大的值,它不适合 char。您如何将该值放入 char 中?其实没有办法!任何事情都可能发生,但最明智的做法是取出该 int 的第一个字节并将其放入 char。分配第一个字节是错误的,但这就是幕后发生的事情。

未指定的行为 这两个函数中的哪个函数首先执行?

void fun(int n, int m);

int fun1() {
    std::cout << "fun1";
    return 1;
}
int fun2() {
    std::cout << "fun2";
    return 2;
}
...
fun(fun1(), fun2()); // which one is executed first?

语言没有指定评估,从左到右或从右到左!因此,未指定的行为可能会也可能不会导致未定义的行为,但您的程序肯定不应该产生未指定的行为。

@eSKay我认为你的问题值得编辑答案以澄清更多:)

为乐趣(乐趣1(),乐趣2());行为不是“实现定义”吗?毕竟,编译器必须选择其中一门课程?

实现定义和未指定之间的区别在于,编译器应该在第一种情况下选择一种行为,但在第二种情况下不必这样做。例如,一个实现必须有一个且只有一个 sizeof(int) 定义。因此,不能说 sizeof(int) 对于程序的某些部分是 4 而对于其他部分是 8。与未指定的行为不同,编译器可以说 OK 我将从左到右评估这些参数,而下一个函数的参数从右到左评估。它可能发生在同一个程序中,这就是为什么它被称为unspecified。事实上,如果指定了一些未指定的行为,C++ 可能会变得更容易。看看这里 Dr. Stroustrup's answer for that

据称,赋予编译器这种自由和要求“普通的从左到右的评估”之间可以产生的差异可能是显着的。我不相信,但是由于无数编译器“在那里”利用了这种自由,而且有些人热情地捍卫这种自由,因此改变将是困难的,并且可能需要数十年才能渗透到 C 和 C++ 世界的遥远角落。令我失望的是,并非所有编译器都会警告诸如 ++i+i++ 之类的代码。同样,未指定参数的评估顺序。 IMO 太多的“事物”未定义,未指定,这很容易说,甚至可以举出例子,但很难解决。还应该注意的是,避免大多数问题并生成可移植代码并不是那么困难。


fun(fun1(), fun2()); 不是行为 "implementation defined"?毕竟,编译器必须选择其中一门课程?
@AraK:感谢您的解释。我现在明白了。顺便说一句,"I am gonna evaluate these arguments left-to-right and the next function's arguments are evaluated right-to-left" 我知道这can会发生。对于我们现在使用的编译器,真的吗?
@eSKay你必须问一个大师,他弄脏了许多编译器:) AFAIK VC总是从右到左评估参数。
@Lazer:肯定会发生。简单场景:foo(bar, boz()) 和 foo(boz(), bar),其中 bar 是 int,boz() 是返回 int 的函数。假设一个 CPU 的参数预计会在寄存器 R0-R1 中传递。函数结果在 R0 中返回;函数可能会破坏 R1。在“boz()”之前评估“bar”需要在调用 boz() 之前将 bar 的副本保存在其他地方,然后加载保存的副本。在 "boz()" 之后评估 "bar" 将避免内存存储和重新获取,并且是许多编译器会执行的优化,无论它们在参数列表中的顺序如何。
我不了解 C++,但 C 标准说将 int 转换为 char 是实现定义的,甚至是定义良好的(取决于类型的实际值和符号)。参见 C99 §6.3.1.3(在 C11 中未更改)。
J
Johannes Schaub - litb

来自官方的 C 基本原理文档

术语未指定的行为、未定义的行为和实现定义的行为用于对编写标准没有或不能完全描述其属性的程序的结果进行分类。采用这种分类的目的是允许实现之间的某种多样性,从而允许实现质量成为市场上的积极力量,并允许某些流行的扩展,而不会消除符合标准的声望。标准附录 F 列出了属于这三类之一的行为。未指定的行为给了实现者一些翻译程序的自由度。这个自由度不会延伸到无法翻译程序的程度。未定义的行为允许实现者不捕获某些难以诊断的程序错误。它还确定了可能的符合语言扩展的领域:实现者可以通过提供官方未定义行为的定义来扩充语言。实现定义的行为使实现者可以自由选择适当的方法,但需要向用户解释该选择。指定为实现定义的行为通常是用户可以根据实现定义做出有意义的编码决策的行为。实施者在决定实施定义的范围时应牢记这一标准。与未指定的行为一样,仅仅未能翻译包含实现定义的行为的源代码并不是一个适当的响应。


超现代编译器编写者还将“未定义行为”视为授予编译器编写者许可,以假设程序永远不会接收会导致未定义行为的输入,并任意更改程序在接收此类输入时的行为方式的所有方面。
我刚刚注意到的另一点:C89 没有使用术语“扩展”来描述在某些实现上保证但在其他实现上没有保证的特性。 C89 的作者认识到,当时的大多数实现将相同地处理有符号算术和无符号算术,除非结果以某些方式使用,并且即使在有符号溢出的情况下也适用这种处理;然而,他们没有将其列为附件 J2 中的常见扩展,这表明他们认为这是一种自然状态,而不是扩展。
A
Anders Abel

Undefined Behavior vs. Unspecified Behavior 对其进行了简短描述。

他们的最后总结:

总而言之,未指定的行为通常是您不应该担心的,除非您的软件需要可移植。相反,未定义的行为总是不可取的,永远不应该发生。


有两种编译器:除非另有明确说明,否则编译器会将标准的大多数未定义行为形式解释为依赖于底层环境记录的特征行为,以及默认情况下仅有用地公开标准描述为的行为的编译器实现定义。当使用第一类编译器时,第一类的许多事情都可以使用 UB 高效且安全地完成。第二种类型的编译器仅适用于此类任务,前提是它们提供了在这种情况下保证行为的选项。
S
Suraj K Thomas

实施定义-

实现者希望,应该有据可查,标准提供选择,但一定要编译

未指定 -

与实现定义但未记录的相同

不明确的-

任何事情都有可能发生,请注意它。


我认为重要的是要注意“未定义”的实际含义在过去几年中发生了变化。过去,给定 uint32_t s;,当 s 为 33 时评估 1u<<s 可能会产生 0 或可能产生 2,但不会做任何其他古怪的事情。然而,较新的编译器评估 1u<<s 可能会导致编译器确定因为 s 必须事先小于 32,所以只有在 s 为 32 或更大时才相关的表达式之前或之后的任何代码可能被省略。
s
supercat

从历史上看,实现定义的行为和未定义的行为都代表了这样一种情况,标准的作者期望编写高质量实现的人会使用判断来决定哪些行为保证(如果有的话)对在预期应用程序领域中运行的程序有用。预期目标。高端数字运算代码的需求与低级系统代码的需求大不相同,UB 和 IDB 都为编译器编写者提供了满足这些不同需求的灵活性。这两个类别都没有要求实现的行为方式对任何特定目的有用,甚至对任何目的都有用。然而,声称适用于特定目的的质量实现,无论标准是否要求,都应该以符合该目的的方式行事。

实现定义的行为和未定义的行为之间的唯一区别是,前者要求实现定义和记录一致的行为,即使在实现可能没有任何用处的情况下也是如此。它们之间的分界线不是定义行为的实现通常是否有用(无论标准是否要求,编译器编写者都应该在可行时定义有用的行为),而是是否可能存在定义行为同时代价高昂的实现和无用的。对此类实现可能存在的判断并不以任何方式、形式或形式暗示对支持在其他平台上定义的行为的有用性的任何判断。

不幸的是,自 1990 年代中期以来,编译器编写者已经开始将缺乏行为要求解释为一种判断,即即使在至关重要的应用领域,甚至在几乎没有成本的系统上,行为保证也不值得付出代价。编译器作者没有将 UB 视为进行合理判断的邀请,而是开始将其视为不这样做的借口。

例如,给定以下代码:

int scaled_velocity(int v, unsigned char pow)
{
  if (v > 250)
    v = 250;
  if (v < -250)
    v = -250;
  return v << pow;
}

二进制补码实现不必花费任何努力将表达式 v << pow 视为二进制补码移位,而无需考虑 v 是正数还是负数。

然而,当今一些编译器编写者的首选理念是,因为 v 只有在程序要进行未定义行为时才能为负数,因此没有理由让程序剪裁 v 的负数范围。尽管过去每个有意义的编译器都支持负值的左移,并且大量现有代码都依赖于这种行为,但现代哲学会解释这样一个事实,即标准说左移负值是 UB暗示编译器编写者应该随意忽略这一点。


但是以一种好的方式处理未定义的行为并不是免费的。现代编译器在某些 UB 情况下表现出这种奇怪行为的全部原因是它们在无情地优化,并且要在这方面做得最好,它们必须能够假设 UB 永远不会发生。
但是 << 在负数上是 UB 的事实是一个令人讨厌的小陷阱,我很高兴被提醒!
@TomSwirly:不幸的是,编译器编写者并不关心提供超出标准规定的松散行为保证,与要求代码不惜一切代价避免标准未定义的任何事情相比,通常可以大幅提高速度。如果程序员不关心在加法溢出的情况下 i+j>k 产生 1 还是 0,只要它没有其他副作用,编译器可能会进行一些大规模优化,而这些优化不会如果程序员将代码编写为 (int)((unsigned)i+j) > k,则可能。
@TomSwirly:对他们来说,如果编译器 X 可以采用严格符合的程序来执行某些任务 T 并产生比编译器 Y 使用相同程序产生的效率高 5% 的可执行文件,这意味着 X 更好,即使 Y给定一个利用 Y 保证但 X 不保证的行为的程序,可以生成执行相同任务的代码效率是其三倍。
@PSkocik:考虑一个简单的场景,其中 ijk 是函数的参数,编译器正在为函数调用 foo(x, y, x) 进行扩展。在这种情况下,编译器可以用 x+y > x 替换 i+j > k,然后它可以用 y > 0 替换,完全跳过加法,消除对 x 值的任何依赖,并可能允许编译器消除如果可以确定 y 将始终为正,则比较和对 y 的确切值的任何依赖关系。
4
4pie0

C++ 标准 n3337 § 1.3.10 实现定义的行为

行为,对于格式良好的程序构造和正确的数据,这取决于实现和每个实现文档

有时,C++ 标准不会对某些结构施加特定的行为,而是说必须通过特定的实现(库的版本)来选择和描述特定的、明确定义的行为。因此,即使标准没有描述这一点,用户仍然可以准确地知道程序的行为方式。

C++ 标准 n3337 § 1.3.24 未定义行为

本国际标准未强加要求的行为 [注:当本国际标准省略任何明确的行为定义或程序使用错误构造或错误数据时,可能会出现未定义的行为。允许的未定义行为的范围从完全忽略具有不可预测结果的情况,到在翻译或程序执行期间以环境特征的记录方式表现(有或没有发出诊断消息),到终止翻译或执行(发出的诊断消息)。许多错误的程序结构不会产生未定义的行为;他们需要被诊断出来。 ——尾注]

当程序遇到未根据 C++ 标准定义的构造时,它可以做任何它想做的事情(可能给我发一封电子邮件,或者给你发一封电子邮件,或者完全忽略代码)。

C++ 标准 n3337 § 1.3.25 未指定行为

行为,对于格式良好的程序构造和正确的数据,这取决于实现 [注意:实现不需要记录发生的行为。可能的行为范围通常由本国际标准描述。 ——尾注]

C++ 标准没有对某些结构施加特定的行为,而是说必须通过特定的实现(库的版本)来选择特定的、明确定义的行为(bot 不必描述)。因此,在没有提供描述的情况下,用户可能很难确切地知道程序的行为方式。


S
Steve Summit

未定义的行为是丑陋的——例如,“好的、坏的和丑陋的”。

好:出于正确的原因,可以编译和工作的程序。

坏:程序有错误,编译器可以检测到并抱怨。

丑陋:一个程序有错误,编译器无法检测和警告,这意味着程序可以编译,并且有时看起来可以正常工作,但有时也会奇怪地失败。这就是未定义的行为。

一些程序语言和其他形式系统努力限制“不确定性的鸿沟”——也就是说,他们试图安排事情,使大多数或所有程序要么“好”,要么“坏”,而很少有“丑陋”的程序”。然而,C 的一个特征是它的“不确定性鸿沟”非常宽。


标准描述为未定义行为的构造是“不可移植的或错误的”,但标准并未试图区分那些错误的构造和那些不可移植但在被编写它们的实现或其他人处理时正确的构造与他们兼容的。