"未定义的行为"真的允许*发生任何事情吗?

Kyl*_*and 93 c c++ undefined-behavior language-lawyer

编辑:这个问题并不是一个讨论未定义行为的(de)优点的论坛,但这就是它的变化.在任何情况下,这个关于假设的C编译器没有未定义行为的线程可能对那些认为这是一个重要主题的人更感兴趣.


当然,"未定义行为"的经典伪装例子是"鼻子恶魔" - 物理上是不可能的,无论C和C++标准允许什么.

因为C和C++社区倾向于强调未定义行为的不可预测性以及允许编译器在遇到未定义行为时使程序完全做任何事情的想法,所以我假设标准没有任何限制关于行为,以及未定义的行为.

但C++标准中的相关引用似乎是:

[C++14: defns.undefined]: [..]允许的未定义行为包括完全忽略不可预测的结果,在转换或程序执行期间以环境特征(有或没有发出诊断消息)的文档方式执行,终止转换或执行(发布诊断信息).[..]

这实际上指定了一小组可能的选项:

  • 忽略这种情况 - 是的,标准继续说这会产生"不可预测的结果",但这与编译器插入代码不同(我认为这是一个先决条件,你知道,鼻子恶魔).
  • 以文件化的方式表现环境 - 这实际上听起来相对温和.(我当然没有听说过任何有关鼻腔恶魔的记录.)
  • 终止翻译或执行 - 使用诊断,不能少.那个UB会表现得那么好吗?

我假设在大多数情况下,编译器选择忽略未定义的行为; 例如,当读取未初始化的内存时,可能是插入任何代码以确保一致行为的反优化.我认为陌生人类型的未定义行为(例如" 时间旅行 ")将属于第二类 - 但这需要记录这些行为并"环境特征"(所以我猜鼻腔恶魔只能由地狱计算机?).

我误解了这个定义吗?这些仅仅是可能构成未定义行为的例子,而不是一个全面的选项列表吗?"任何可能发生的事情"的说法仅仅意味着忽视这种情况的意外副作用吗?

编辑:两个小问题澄清:

  • 我认为从原始问题中可以清楚地看出,我认为对大多数人来说都是如此,但无论如何我都会拼出来:我确实意识到"鼻子恶魔"是诙谐的.
  • 请不要写的(其他)答案解释UB允许特定于平台的编译器优化,除非你也解释它是如何允许的优化,实现定义的行为也不会允许.

Meh*_*dad 77

是的,它允许任何事情发生.这个说明只是举例.定义很清楚:

未定义的行为:本国际标准没有要求的行为.

  • @supercat示例和注释不是规范性的. (12认同)
  • Gaaaaaaah,我错过了那条线,并且(显然)将我引用的位解释为实际定义. (5认同)
  • @supercat:很明显,意图主要是"确定一个程序不会收到什么输入" - 这只是编译器当时没那么先进.例如,当`n`等于或超过`x`类型的宽度时,'x << n`的整个点是UB,编译器可以简单地假设`n`不是,而不是必须为这种情况下的操作实施复杂且昂贵的逻辑.从概念上讲,在进行此优化和基于UB执行其他更高级的DCE方面没有区别. (5认同)
  • 然而,有趣的是,将可能反映该短语的预期含义的规范性例子与现代编译器的行为进行比较.我没有看到任何证据表明标准的作者希望编译器使用未定义的行为来确定程序将会或不会接收什么输入. (3认同)
  • @R ..:我会用"<<"来解释UB的意图,因为"程序员不会在字大小*之上使用`y << y`值,除非在为这样的行为编写平台时一个动作满足他们的要求*.当为平台编写代码以保证将`uint32_t`从32移动到127的任何值将产生零时,利用这种行为可以允许某些类型的代码比可能的更有效如果必须为这些值添加条件逻辑.更值得注意的是,带有无关指针`p`和`q`的`p <q`会产生UB ... (2认同)
  • @supercat 除非我们知道 `base` 是一个长度为 `length` 的对象的开始,用 `true` 替换 `p &gt;= base &amp;&amp; p &lt; base+length` 比实际使用位更有效和东西,它会符合。这比实际完成工作要快得多。无限次减速肯定是不切实际的! (2认同)
  • @supercat不,我是说您对“实用”的使用不切实际。当然可以,看到它便会知道。如今,编译器可以自由声明其指针存在于平面存储空间中。一些编译器选择不做超出标准的(许多)保证,而是利​​用这种自由。其他编译器则没有。实用的程序员必须使用一种标准将其代码限制为一种编译器的一种版本,或者针对该标准进行编码。尝试仅通过大量警告进入未定义的行为,如果收益很大,则最好声明编译器版本。 (2认同)
  • @supercat:您忽略了使 UB 的影响变得不受欢迎的最大好处:破坏不可移植的代码并迫使人们编写可移植的代码。其价值不可低估。 (2认同)
  • @supercat:我不相信不可能编写可移植代码的论点。在没有 UB 的情况下编写代码当然是实用的,并且只依赖于以编译时可测试的方式实现定义的行为,例如测试`CHAR_BIT==8`、`type_MIN!=-type_MAX`(2s 补码/全- range)、`defined(UINTn_MAX)` 等。与依赖未定义的行为被赋予不可测试的属性相比,这种情况要危险得多,你希望它在现实世界的系统上具有不可测试的属性。 (2认同)
  • @supercat:现在您要进入一个单独的主题,该主题使语言的功能蔓延。对 C 的功能请求与用户一样多,但即使其中 1% 被娱乐,它也会像 C++ 一样一团糟.. :-) 无论如何,这是与 UB 不同的主题。 (2认同)

sup*_*cat 23

未定义行为的历史目的之一是允许某些行为可能在不同平台上具有不同的潜在有用效果.例如,在C的早期,给出

int i=INT_MAX;
i++;
printf("%d",i);
Run Code Online (Sandbox Code Playgroud)

一些编译器可以保证代码将打印某些特定值(对于二进制补码机器,它通常是INT_MIN),而其他编译器可以保证程序在没有到达printf的情况下终止.根据应用要求,任一行为都可能有用.保持行为未定义意味着异常程序终止是溢出的可接受结果,但产生看似有效但错误的输出的应用程序不会,如果在可靠地捕获它的平台上运行则可以放弃溢出检查,以及应用程序在溢出的情况下异常终止是不可接受的,但产生算术不正确的输出,如果在没有捕获溢出的平台上运行,可能会放弃溢出检查.

然而,最近,一些编译器作者似乎已经参加了一场竞赛,看谁能最有效地消除任何代码,而这些代码的存在不会被标准规定.例如,鉴于......

#include <stdio.h>

int main(void)
{
  int ch = getchar();
  if (ch < 74)
    printf("Hey there!");
  else
    printf("%d",ch*ch*ch*ch*ch);
}
Run Code Online (Sandbox Code Playgroud)

超现代编译器可以得出结论,如果ch是74或更大,计算ch*ch*ch*ch*ch将产生未定义的行为,因此程序应该打印"嘿那里!" 无条件地无论输入什么字符.

  • 不,这是关于可移植性的.我们现在生活在一个相互关联的时代,软件的分发速度比您想象的要快.我们不再为地下室那个尘土飞扬的超级计算机编写程序了.至少,我们大多数人不是.它有效地延续了数十年的编程范式转变; 现在,严格按照标准编码(理想情况下我们一直都是这样做)有实际的实际好处,工具链编写者可以利用它来生成真正快速有效的编译器.为什么不?! (10认同)
  • @LightnessRacesinOrbit:如果目标是有一个可用的可移植语言,委员会应该认识到存在一些不同的变体(例如`p> = object.base && p <object.base + object.size)`的方言测试`p`是否是一个对象的一部分,但是不能在所有平台上实现,而不是那些不允许进行这种比较但可以在更多平台上实现的对象.它还应该定义一些数据类型,如果支持这些数据类型,则需要在所有平台上表现一致.实际上,C有两个不同的32位有符号整数类型...... (4认同)
  • 哇.任何想法我们如何从"潜在有用"到当前的情况,其中许多C++社区似乎坚决反对任何尝试确定某些编译器在遇到允许UB的情况时的确切行为,并解释"它没有"重要的是,你的程序有UB"? (3认同)
  • ...和两个不同的无符号32位整数类型.在`uint32_t`的所有值都可表示为`int`的平台上,减去两个`uint32_t`值将产生一个带符号的结果.在某些`uint32_t`值不能表示为`int`的平台上,减法会产生一个`uint32_t`结果.这两种类型都称为`uint32_t`,但它们的语义极为不同.同样,在`int`大于32位的平台上,递增`int32_t`将始终具有已定义的行为.在`int`正好是32位的平台上,递增`int32_t`会导致UB. (3认同)
  • @LightnessRacesinOrbit:此外,一种可移植的语言应该定义一种有效的可移植方法,用于将较大的整数类型打包和解包到较小的整数类型中.写`*dat ++ = value&255;*dat ++ =(value >> 8)&255;*dat ++ =(value >> 16)&255;*DAT ++ =(值>> 24)&255;`可以是100%的便携式(即使对于机器其中`CHAR_BITS> 8`,但即使在平台,其中单个32比特存储会产生正确的行为就难以对一个编译器来确定它.给定`__pack_i32_cle(&dat,value);`任何编译器都可以轻松生成最佳代码. (2认同)
  • 我认为很难证明UB的存在是正确的; 它的缺点不仅仅是好处 (2认同)
  • @Giorgi有数百种没有UB的语言,也许改用其中一种,而不是尝试使所有语言都相同 (2认同)
  • @MM 但是很多人(比如我自己)出于需要使用 C 或 C++;更多的人(实际上很可能任何人出于任何目的使用现代计算机)都使用用 C 和 C++ 编写的程序,我怀疑每个人都熟悉的许多神秘的错误/错误/崩溃/等都是由于 UB。 (2认同)

小智 16

挑剔:你没有引用标准.

这些是用于生成C++标准草稿的来源.这些来源不应被视为ISO出版物,除非C++工作组(ISO/IEC JTC1/SC22/WG21)正式采用,否则也不应将这些来源视为ISO出版物.

解释:根据ISO/IEC指令第2部分,注释不是规范性的.

集成在文档文本中的注释和示例仅用于提供有助于理解或使用文档的其他信息.它们不应包含要求("应";见3.3.1和表H.1)或任何被认为对文件使用必不可少的信息,例如说明(命令性;见表H.1),建议("应该";见3.3.2和表H.2)或许可("可";见表H.3).注释可以写成事实陈述.

强调我的.仅此就排除了"综合选项列表".然而,给出的例子确实算作"旨在帮助理解文件的附加信息".

请记住,"鼻妖"模因并不是字面意思,就像使用气球来解释宇宙膨胀如何在物理现实中保持真理一样.这是为了说明当允许做任何事情时讨论"未定义的行为" 应该做什么是蛮干的.是的,这意味着外太空没有实际的橡皮筋.

  • 这些标准的最终版本不是免费提供的,但是在相当高的付费墙背后,因此无法链接.但是,最终草案与所有相关技术和语言方面的最终版本相同.没有这些草案,引用和引用标准实际上是不可能的.那么您更喜欢什么:1)从最终(以及在相同方面)相同的引文中引用或2)根本没有引用,因此只是说没有基础?(你怎么知道空间里没有**橡皮筋?) (2认同)

Pet*_*ter 12

在每个C和C++标准中,未定义行为的定义基本上是标准对发生的事情没有要求.

是的,这意味着任何结果都是允许的.但是没有特定的结果需要发生,也没有任何结果不需要发生.如果你有一个编译器和库一致地产生一个特定行为以响应特定的未定义行为实例 - 这样的行为不是必需的,并且即使在编译器的未来错误修复版本中也可能会改变 - 并且编译器也没关系根据C和C++标准的每个版本,仍然完全正确.

如果您的主机系统具有与插入到您的鼻孔中的探针的连接形式的硬件支持,那么发生未定义的行为可能会导致不希望的鼻效应.

  • 从历史上看,标准没有定义行为的事实绝不意味着实现不应该这样做.实际上,触发未定义行为的许多事情都是这样做的,因为在批准C标准之前,不同的实现产生了两个(或更多)相互矛盾的保证,这两个保证都是为这些实现编写的程序所依赖的. (5认同)
  • @supercat但我相信标准分别定义了"实现定义"行为,它与您所说的相符.例如,对签名值做什么>>是实现定义的(这意味着必须在编译器文档中发生一致和定义的东西),而对于有符号值的<<未定义(这意味着任何事情都可能发生,没有人必须定义) .不要责怪编译器编写者; 很明显,标准的现代编写者对正在发生的事情完全没有问题,否则他们只会定义所有当前未定义的行为实现! (3认同)
  • 一个很好的例子是取消引用空指针。在SPARC上,读取的值为0,而在写入时则默默地丢弃结果。在MS-DOS上,该位置保存了中断表。尝试对帐_that_。 (2认同)

Muz*_*zer 8

我以为我只会回答你的一个观点,因为其他答案很好地回答了一般性问题,但是还没有解决这个问题.

"忽略这种情况 - 是的,标准继续说这将产生"不可预测的结果",但这与编译器插入代码不同(我认为这是一个先决条件,你知道,鼻子恶魔). "

在没有编译器插入任何代码的情况下,使用合理的编译器可以非常合理地预期会出现鼻子恶魔的情况如下:

if(!spawn_of_satan)
    printf("Random debug value: %i\n", *x); // oops, null pointer deference
    nasal_angels();
else
    nasal_demons();
Run Code Online (Sandbox Code Playgroud)

如果编译器可以证明*x是空指针解除引用,则完全有权作为某些优化的一部分,说"好的,所以我看到他们在if的这个分支中取消引用了一个空指针.因此,作为该分支的一部分,我可以做任何事情.因此我可以对此进行优化:"

if(!spawn_of_satan)
    nasal_demons();
else
    nasal_demons();
Run Code Online (Sandbox Code Playgroud)

"从那里,我可以优化到这个:"

nasal_demons();
Run Code Online (Sandbox Code Playgroud)

您可以看到,在适当的情况下,这种事情如何证明对优化编译器非常有用,但却会造成灾难.我确实在一段时间后看到了一些例子,其中实际上优化能够优化这种情况非常重要.当我有更多时间时,我可能会尝试将它们挖出来.

编辑:一个例子来自我对这种优化有用的情况的记忆深度,你经常检查一个指针是否为NULL(可能是内联辅助函数),即使已经取消引用它并且没有改变了 优化编译器可以看到你已经取消引用它并因此优化了所有"is NULL"检查,因为如果你已经取消引用它并且它是null,则允许发生任何事情,包括不运行"is NULL"检查.我相信类似的论点适用于其他未定义的行为.


ala*_*ain 7

首先,需要注意的是,不仅未定义用户程序的行为,而且未定义编译器的行为.类似地,UB在运行时不会遇到,它是源代码的属性.

对于编译器编写者来说,"行为未定义"意味着"你不必考虑这种情况",甚至"你可以假设没有源代码会产生这种情况".当与UB一起呈现时,编译器可以有意或无意地执行任何操作,并且仍然符合标准,所以是的,如果您授予了访问权限......

然后,并不总是能够知道程序是否具有UB.例:

int * ptr = calculateAddress();
int i = *ptr;
Run Code Online (Sandbox Code Playgroud)

知道这是否可能是UB将需要知道返回的所有可能值calculateAddress(),这在一般情况下是不可能的(参见" 暂停问题 ").编译器有两种选择:

  • 假设ptr将始终具有有效地址
  • 插入运行时检查以保证某种行为

第一个选项产生快速程序,并为程序员提供避免不良影响的负担,而第二个选项产生更安全但速度更慢的代码.

C和C++标准使这个选择开放,大多数编译器选择第一个,而Java例如强制要求第二个.


为什么行为不是实现定义的,而是未定义的?

实现定义装置(N4296,1.9§2):

抽象机的某些方面和操作在本国际标准中描述为实现定义的(例如,sizeof(int)).这些构成了抽象机器的参数.每个实施应包括描述其在这些方面的特征和行为的文件.此类文档应定义与该实现相对应的抽象机器的实例(下面称为"相应实例").

强调我的.换句话说:当源代码使用实现定义的功能时,编译器编写者必须准确记录机器代码的行为.

写入随机非空无效指针是您可以在程序中执行的最不可预测的事情之一,因此这也需要降低性能的运行时检查.
在我们拥有MMU之前,您可以通过写入错误的地址来破坏硬件,这非常接近于鼻子恶魔;-)

  • 编译器的行为并非未定义。编译器不应该格式化你的硬盘,或者发射导弹,或者崩溃。未定义的是编译器生成的可执行文件(如果有)的行为。 (2认同)
  • “在运行时未遇到UB,它是源代码的属性。” -两种都有。UB可能会在运行时遇到,例如用用户除以整数输入而未检查他们是否未输入“ 0” (2认同)

归档时间:

查看次数:

6796 次

最近记录:

7 年,7 月 前