如果您在多个平台上部署,未定义的行为只是一个问题?

大多数围绕未定义行为 (UB)的对话都谈论如何有一些平台可以做到这一点,或者一些编译器这样做。

如果你只对一个平台感兴趣而且只有一个编译器(同一版本),而且你知道你会使用它们多年?

没有什么改变,但代码和UB没有实现定义。

一旦UB已经体现了这种架构,编译器和你已经testing过了,你不能假设从那时起,无论编译器第一次使用UB,每次都会这样做吗?

注意:我知道未定义的行为是非常非常糟糕的 ,但是当我在这种情况下用某人编写的代码指出UB时,他们问了这个问题,我没有什么比这更好的了,如果你需要升级或者港口,所有的UB将是非常昂贵的修复。

看来有不同的行为类别:

  1. Defined – 这是由标准logging的行为
  2. Supported – 这是Supported行为logging的行为定义
  3. Extensions – 这是一个文档的补充,支持低级别的操作,如popcount ,分支提示,属于这个类别
  4. Constant – 虽然没有logging,但是这些行为可能会在给定的平台上是一致的,如endianness, sizeof int而不是可移植的可能不会改变
  5. Reasonable – 通常是安全的,通常是遗留的,从无符号到有符号的转换,使用指针的低位作为临时空间
  6. Dangerous – 读取未初始化或未分配的内存,返回一个临时variables,在非pod类上使用memcopy

看起来Constant在一个平台上的补丁版本中可能是不变的。 “ Reasonable和“ Dangerous之间的界限似乎越来越趋于Dangerous因为编译器在优化方面变得更加积极

操作系统更改,无害的系统更改(不同的硬件版本!)或编译器更改都可能导致以前“工作”的UB无法工作。

但比这更糟糕。

有时,对不相关的编译单元或同一编译单元中较远的代码的更改可能会导致以前“工作”的UB无法工作; 作为例子,两个内联函数或方法具有不同的定义但签名相同。 一个在连接过程中被无声丢弃; 而完全无害的代码更改可以更改哪个被丢弃。

在一个上下文中工作的代码在不同的上下文中使用时,会突然停止在同一个编译器,操作系统和硬件上工作。 一个例子是违反强烈的别名; 编译的代码可能在A点被调用时工作,但在内联(可能在链接时!)代码可以改变意义。

你的代码,如果是一个更大的项目的一部分,可以有条件地调用一些第三方代码(比如说,一个在文件打开对话框中预览图像types的shell扩展),它改变了一些标志的状态(浮点精度,区域设置,整数溢出标志,零行为划分等)。 你的代码,以前工作得很好,现在显示完全不同的行为。

其次,多种未定义的行为本质上是不确定的。 在释放指针后访问指针的内容(甚至写入指针)可能是安全的99/100,但是换出1/100的页面,或者在写入之前写入了其他内容。 现在你有内存损坏。 它通过了所有的testing,但是对于哪些地方可能会出现问题还缺乏完整的知识。

通过使用未定义的行为,您可以完全理解C ++标准,编译器在这种情况下可以执行的所有操作,以及运行时环境可以做出的各种反应。 您必须审核生成的程序集,而不是C ++源代码,可能是整个程序,每次构build它! 您还可以将所有读取该代码的人员或修改该代码的人员提交到该知识水平。

有时候还是值得的。

最快可能的代表使用UB和关于调用约定的知识是一个非常快速的非拥有std::functiontypes的types。

不可能的快速代表竞争。 在某些情况下速度更快,其他情况下速度更慢,并符合C ++标准。

使用UB可能是值得的,性能提升。 除了这种UB hackery的性能(速度或内存使用)之外,你很less能获得其他的东西。

我见过的另一个例子是,当我们不得不注册一个只有一个函数指针的C API的callback。 我们将创build一个函数(编译时没有优化),将其复制到另一个页面,修改该函数中的指针常量,然后将该页面标记为可执行文件,从而允许我们将函数指针与指针一起秘密传递给callback函数。

另一种实现方式是使用一些固定大小的函数集(10?100?1000?100万?),所有这些std::function在全局数组中查找std::function并调用它。 这将限制我们在同一时间安装多less次这样的callback,但实际上已经足够了。

不,这不安全。 首先,你将不得不修复所有的东西 ,不仅仅是编译器版本。 我没有特别的例子,但我猜测一个不同的(升级的)操作系统,甚至升级的处理器可能会改变UB的结果。

而且,即使有不同的数据input到你的程序,也可能改变UB行为。 例如,一个超出边界的数组访问(至less不进行优化)通常取决于数组之后的内存中的任何内容。 UPD :看到Yakk在这方面的更多讨论的一个很好的答案。

而更大的问题是优化和其他编译器标志。 UB可能会根据优化标志以不同的方式显示自己,并且很难想象有人总是使用相同的优化标志(至less您将使用不同的标志来进行debugging和发布)。

UPD :刚刚注意到你从来没有提到修复编译器版本 ,你只提到修复一个编译器本身。 那么一切都变得更加不安全:新的编译器版本肯定会改变UB行为。 从这一系列的博客文章 :

可怕的是,几乎所有基于未定义行为的优化都可能在未来的任何时候开始被错误的代码触发。 内联,循环展开,内存升级和其他优化将继续变得更好,其存在的重要原因是揭示次优化。

这基本上是一个关于特定C ++实现的问题。 “我能否假设一个未定义的特定行为将继续由平台XYZ上的$($ CXX)在UVW的情况下以相同的方式处理?”

我认为你要么应该明确说明你正在使用的编译器和平台,然后查阅他们的文档,看看他们是否做出任何保证,否则这个问题是根本无法回答的。

未定义的行为的整个观点是,C ++标准没有规定发生了什么,所以如果你正在寻找某种标准的保证,那就是“ok”,你不会去find它。 如果你问“整个社区”是否认为安全,那主要是基于意见的。

一旦UB已经体现了这种架构,编译器和你已经testing过了,你不能假设从那时起,无论编译器第一次使用UB,每次都会这样做吗?

只有编译器制造商保证你能做到这一点,否则,不,这是一厢情愿的想法。


让我试着以稍微不同的方式再次回答。

众所周知,在正常的软件工程和大型工程中,程序员/工程师被教导按照标准来做事情,编译器编写者/零件制造者生产符合标准的零件/工具,最后生产出一些东西在“根据标准的假设下,我的工程工作表明这个产品将起作用”,然后你testing它并运送它。

假设你有一个疯狂的叔叔jimbo,有一天,他把所有的工具都拿出来,一大堆两四个人,工作了几个星期,在你家后院做了一个临时的过山车。 然后你运行它,果然它不会崩溃。 而且你甚至可以运行十次,而且不会崩溃。 现在jimbo不是工程师,所以这不是按照标准来做的。 但是,如果十次之后都没有崩溃,那就意味着它是安全的,你可以开始向公众收费,对吗?

在很大程度上什么是安全的,什么不是一个社会学问题。 但如果你只想提出一个简单的问题:“我什么时候可以合理地认为没有人会因为我收费而受到伤害,当我不能真正假设产品的时候”,这就是我该怎么做的。 假设我估计,如果我开始向公众收费,我会运行X年,在那个时候,可能会有10万人乘坐它。 如果它基本上是一个有偏见的硬币翻转是否打破,那么我想看到的是类似的东西,“这个设备已经运行了一百万次碰撞假人,它从来没有崩溃或显示破解的提示。 那么我可以相当合理地相信,如果我开始向公众收费,即使没有严格的工程标准,任何人都会受到伤害的几率是很低的。 这只是基于对统计和力学的一般了解。

关于你的问题,我想说,如果你的代码有未定义的行为,标准,编译器制造商或者其他人都不会支持,那么基本上就是“疯狂的叔叔jimbo”工程,而这只是“好吧“,如果你做了大量增加的testing来validation它是否满足你的需求,基于统计和计算机的一般知识。

你指的是更可能实现的定义,而不是未定义的行为 。 前者是当标准没有告诉你会发生什么,但如果你使用相同的编译器和相同的平台,它应该是相同的。 一个例子是假定int是4个字节长。 UB是更严重的事情。 那里的标准没有说什么。 对于一个给定的编译器和平台,它可能是有效的,但也有可能它只在某些情况下才起作用。

一个例子是使用未初始化的值。 如果你在一个if使用了一个未初始化的bool ,你可能会得到真或假,并且可能会发生它总是你想要的,但是代码会以几种令人惊讶的方式破解。

另一个例子是解引用空指针。 尽pipe在所有情况下都可能导致段错误,但是标准并不要求程序每次运行程序时都会产生相同的结果。

总之,如果你正在做一些已经定义好的实现 ,那么如果你只是开发一个平台而且你testing了它的工作,那么你是安全的。 如果你正在做一些未定义的行为 ,那么在任何情况下你都可能不安全。 它可能有效,但没有保证。

想一想另一种方式。

未定义的行为永远不好,永远不要使用,因为你永远不知道你会得到什么。

不过,你可以用这个来锻炼

行为可以由除了语言规范之外的各方定义

因此,你永远不应该依赖UB,但是你可以find其他的来源,说明在你的情况下,你的编译器的某种行为是DEFINED行为。

对于快速的代表class,雅克举了很多例子。 在这些情况下,根据规范,作者明确声称他们正在进行不确定的行为。 然而,他们然后去解释一个商业原因,为什么这个行为比这个更好定义。 例如,他们声明成员函数指针的内存布局在Visual Studio中不太可能发生变化,因为由于不兼容而导致的业务成本太高,这对微软来说是不合适的。 因此他们宣称行为是“事实上定义的行为”。

类似的行为可以在pthreads的典型linux实现中看到(由gcc编译)。 在某些情况下,他们会假设在multithreading场景中允许编译器调用哪些优化。 这些假设在源代码中的评论中清楚地陈述。 这个“事实上定义的行为”是怎么样的? 那么,pthreads和gcc就可以携手合作了。 为gcc打开pthreads添加一个优化是不能接受的,所以没有人会做到这一点。

但是,你不能做出相同的假设。 你可能会说“pthreads做到了,所以我应该也能做到”。 然后,有人进行优化,并更新gcc来处理它(可能使用__sync调用,而不是依靠volatile )。 现在pthreads一直在运行……但是你的代码已经不在了。

还要考虑一下MySQL的情况(或者是Postgre?),他们发现缓冲区溢出错误。 溢出实际上已经被代码捕获,但是它使用了未定义的行为,所以最新的gcc开始优化整个检出。

所以,总而言之,寻找定义行为的替代来源,而不是在未定义的情况下使用它。 find1.0 / 0.0等于NaN的原因是完全合法的,而不是引起浮点陷阱发生。 但是,如果没有首先certificate它是您和您的编译器的有效行为定义,请不要使用该假设。

请哦,请记住,我们现在升级编译器。

从历史上看,即使“标准”没有要求,C编译器通常也会以某种可预测的方式行事。 例如,在大多数平台上,一个空指针和一个死对象指针之间的比较只会报告它们不相等(如果代码希望安全地断言指针为空,并且如果不是,则非常有用)。 标准并不要求编译器做这些事情,但历史上编译器可以很容易地做到这一点。

不幸的是,一些编译器编写者已经意识到,如果在指针有效非空时无法达到这种比较,编译器应该省略断言代码。 更糟糕的是,如果它也可以确定某个input会导致代码到达一个无效的非空指针,它应该假设这样的input将永远不会被接收,并省略所有的代码来处理这样的input。

希望这样的编译器行为将变成一个短暂的时尚。 假设它是由“优化”代码的愿望驱动的,但是对于大多数应用程序来说,鲁棒性比速度更重要,并且编译器会混淆代码,从而限制错误input或差事程序行为造成的损害,这是一种灾难。

然而在此之前,在使用编译器仔细阅读文档时必须非常小心,因为不能保证编译器作者不会认为支持有用的行为并不那么重要,尽pipe这些行为得到广泛支持, (比如能够安全地检查两个任意对象是否重叠),而不是利用每个机会来消除标准不要求执行的代码。

未定义的行为可以通过诸如环境温度之类的事情来改变,这会导致旋转硬盘的延迟发生变化,从而导致线程调度发生改变,进而改变被评估的随机垃圾的内容。

总之,除非编译器或操作系统指定行为(因为语言标准没有),否则不安全。

未定义的任何types的行为都有一个基本的问题:它被卫生消毒和优化者诊断。 编译器可以默默地改变对应于从一个版本到另一个版本的行为(例如,通过扩展它的库),突然之间你的程序中会有一些不可追踪的错误。 这应该避免。

虽然有一些未定义的行为是由您的特定实现“定义”的。 左移一个负位数可以由您的机器定义,并且在那里使用它是安全的,因为logging的特性的突然变化很less出现。 另一个常见的例子是严格的别名 :GCC可以通过-fno-strict-aliasing禁用这个限制。

虽然我同意这样的答案,即使不针对多个平台也不安全,但每个规则都可能有例外。

我想介绍两个例子,我相信允许未定义/实现定义的行为是正确的select。

  1. 一个单一的程序。 这不是一个旨在被任何人使用的程序,但它是一个小而快速的书面程序,用来计算或生成一些东西。 在这种情况下,如果我知道我的系统的字节顺序,并且我不想编写一个与其他字节序相关的代码,那么“快速和肮脏”的解决scheme就是正确的select。 例如,我只需要它来执行一个mathcertificate,知道我能否在我的另一个面向用户的程序中使用特定的公式。

  2. 非常小的embedded式设备。 最便宜的微控制器有几百字节的内存。 如果你开发一个闪烁的LED或音乐明信片等的小玩具,每一分钱都很重要,因为它将以每单位利润非常低的数百万产生。 处理器和代码都不会改变,如果您不得不为下一代产品使用不同的处理器,那么您可能不得不重写代码。 在这种情况下,未定义行为的一个很好的例子就是在上电时,每个存储单元都有一个微控制器保证零值(或255)。 在这种情况下,您可以跳过variables的初始化。 如果你的微控制器只有256个字节的内存,这可以使一个适合内存的程序和一个不适合的代码有所不同。

任何人不同意第二点,请想象一下,如果你向你的老板说了这样的话会怎么样?

“我知道硬件成本只有0.40美元,而我们计划以0.50美元的价格出售,但是我为它编写的40行代码的程序只适用于这种特定types的处理器,所以如果在遥远的将来我们换一个不同的处理器,代码将不能使用,我不得不把它扔出来,写一个新的。一个适用于每一种types的处理器的标准符合的程序将不适合我们的$ 0.40处理器。我要求使用价格为0.60美元的处理器,因为我拒绝写一个不便携的程序。

“没有改变的软件没有被使用。”

如果你正在用指针做一些不寻常的事情,可能有一种方法可以使用强制转换来定义你想要的。 由于它们的性质,它们不会是“编译器第一次和UB做什么”。 例如,当您引用由未初始化指针指向的内存时,每次运行该程序时都会得到一个不同的随机地址。

未定义的行为通常意味着你正在做一些棘手的事情,并且以另一种方式完成任务会更好。 例如,这是未定义的:

 printf("%d %d", ++i, ++i); 

很难知道什么意图甚至会在这里,应该重新思考。

Changing the code without breaking it requires reading and understanding the current code. Relying on undefined behavior hurts readability: If I can't look it up, how am I supposed to know what the code does?

While portability of the program might not be an issue, portability of the programmers might be. If you need to hire someone to maintain the program, you'll want to be able to look simply for a ' <language x> developer with experience in <application domain> ' that fits well into your team rather than having to find a capable ' <language x> developer with experience in <application domain> knowing (or willing to learn) all the undefined behavior intrinsics of version xyz on platform foo when used in combination with bar while having baz on the furbleblawup ' .

Nothing is changing but the code, and the UB is not implementation-defined.

Changing the code is sufficient to trigger different behavior from the optimizer with respect to undefined behavior and so code that may have worked can easily break due to seemingly minor changes that expose more optimization opportunities. For example a change that allows a function to be inlined, this is covered well in What Every C Programmer Should Know About Undefined Behavior #2/3 which says:

While this is intentionally a simple and contrived example, this sort of thing happens all the time with inlining: inlining a function often exposes a number of secondary optimization opportunities. This means that if the optimizer decides to inline a function, a variety of local optimizations can kick in, which change the behavior of the code. This is both perfectly valid according to the standard, and important for performance in practice.

Compiler vendors have become very aggressive with optimizations around undefined behavior and upgrades can expose previously unexploited code:

The important and scary thing to realize is that just about any optimization based on undefined behavior can start being triggered on buggy code at any time in the future. Inlining, loop unrolling, memory promotion and other optimizations will keep getting better, and a significant part of their reason for existing is to expose secondary optimizations like the ones above.