Gle*_*aum 51 c++ undefined-behavior
围绕未定义行为(UB)的大多数对话都讨论了如何有一些平台可以做到这一点,或者一些编译器会这样做.
What if you are only interested in one platform and only one compiler (same version) and you know you will be using them for years?
Nothing is changing but the code, and the UB is not implementation-defined.
Once the UB has manifested for that architecture and that compiler and you have tested, can't you assume that from then on whatever the compiler did with the UB the first time, it will do that every time?
Note: I know undefined behavior is very, very bad, but when I pointed out UB in code written by somebody in this situation, they asked this, and I didn't have anything better to say than, if you ever have to upgrade or port, all the UB will be very expensive to fix.
It seems there are different categories of Behavior:
Defined - This is behavior documented to work by the standardsSupported - This is behavior documented to be supported a.k.a
implementation definedExtensions - This is a documented addition, support for low level
bit operations like popcount, branch hints, fall into this categoryConstant - While not documented, these are behaviors that will
likely be consistent on a given platform things like endianness,
sizeof int while not portable are likely to not changeReasonable - generally safe and usually legacy, casting from
unsigned to signed, using the low bit of a pointer as temp spaceDangerous - reading uninitialized or unallocated memory, returning
a temp variable, using memcopy on a non pod classConstant在一个平台上的补丁版本中似乎可能是不变的.之间的界限Reasonable,并Dangerous似乎正朝着越来越多的行为,Dangerous如编译器成了他们的优化更积极
Yak*_*ont 56
操作系统更改,无害的系统更改(不同的硬件版本!)或编译器更改都可能导致以前"工作"UB无法正常工作.
但它比那更糟糕.
有时,对不相关的编译单元的更改或同一编译单元中的远程代码可能会导致以前"工作"的UB无法工作; 例如,两个内联函数或方法具有不同的定义但具有相同的签名.一个人在连接过程中被默默地丢弃; 完全无害的代码更改可以改变哪一个被丢弃.
当您在不同的上下文中使用时,在一个上下文中工作的代码可能突然停止在相同的编译器,操作系统和硬件中工作.一个例子是违反强混叠; 编译后的代码可能在现场A调用时有效,但是当内联时(可能在链接时!),代码可以改变含义.
您的代码(如果是较大项目的一部分)可以有条件地调用某些第三方代码(例如,在文件打开对话框中预览图像类型的shell扩展),这些代码会更改某些标志的状态(浮点精度,区域设置,整数溢出)标志,按零行为划分等).您之前运行良好的代码现在表现出完全不同的行为.
接下来,许多种未定义的行为本质上是非确定性的.在释放指针(甚至写入指针)之后访问指针的内容可能是安全的99/100,但是在交换之前,页面被换出了1/100或其他内容.现在你有内存损坏.它通过了你所有的测试,但是你缺乏对错误的全面了解.
通过使用未定义的行为,您可以完全理解C++标准,编译器在这种情况下可以执行的所有操作,以及运行时环境可以做出的各种反应.每次构建它时,您都必须审核生成的程序集,而不是C++源代码,可能是整个程序!您还将读取该代码或修改该代码的每个人提交到该级别的知识.
有时它仍然值得.
最快可能的代理使用UB和关于调用约定的知识是一种非常快速的非拥有std::function类型.
不可能快速的代表竞争.它在某些情况下更快,在其他情况下更慢,并且符合C++标准.
为了提高性能,使用UB可能是值得的.你很难从这种UB hackery获得性能(速度或内存使用)以外的东西.
我看到的另一个例子是当我们必须使用一个只带有函数指针的不良C API注册一个回调时.我们创建一个函数(在没有优化的情况下编译),将其复制到另一个页面,修改该函数中的指针常量,然后将该页面标记为可执行,允许我们将指针与函数指针一起秘密传递给回调.
另一种实现方式是使用一些固定大小的函数集(10?100?1000?100万?),所有函数都std::function在全局数组中查找并调用它.这将限制我们在任何时候安装的回调数量,但实际上已经足够了.
Pet*_*etr 20
不,那不安全.首先,您必须修复所有内容,而不仅仅是编译器版本.我没有特定的例子,但我想不同的(升级的)操作系统,甚至升级的处理器可能会改变UB结果.
此外,即使为程序输入不同的数据也可能会改变UB行为.例如,一个越界数组访问(至少没有优化)通常取决于数组后内存中的内容. UPD:看到Yakk的一个很好的答案,可以对此进行更多讨论.
更大的问题是优化和其他编译器标志.根据优化标志,UB可能以不同的方式表现出来,并且很难想象有人总是使用相同的优化标志(至少你会使用不同的标志进行调试和释放).
UPD:刚刚注意到你从未提及修复编译器版本,你只提到修复编译器本身.然后一切都更不安全:新的编译器版本肯定会改变UB的行为.从这一系列的博客文章:
要实现的重要且可怕的事情是, 基于未定义行为的任何优化都可以在将来的任何时间开始在错误代码上触发.内联,循环展开,内存促销和其他优化将继续变得更好,并且他们存在的重要部分原因是暴露上述二次优化.
这基本上是关于特定C++实现的问题."我能否假设在UVW情况下,平台XYZ上的($ CXX)将以同样的方式继续处理标准未定义的特定行为?"
我想你要么明确说明你正在使用什么编译器和平台,然后查阅他们的文档,看看他们是否做出任何保证,否则这个问题从根本上说是无法回答的.
未定义行为的全部意义在于C++标准没有指定发生了什么,因此如果您正在寻找标准的某种保证,那就是"好",您将无法找到它.如果您在询问"整个社区"是否认为安全,那主要是基于意见.
一旦UB已经体现了该体系结构以及您已经测试过的编译器,那么你不能假设从那时起编译器第一次对UB做了什么,它每次都会这样做吗?
只有编译器制造商保证你能做到这一点,否则,不,这是一厢情愿的想法.
让我尝试以稍微不同的方式再次回答.
众所周知,在正常的软件工程和整个工程中,程序员/工程师被教导按照标准做事,编译器编写者/零件制造商生产符合标准的零件/工具,最后你生产出一些东西在"根据标准的假设,我的工程工作表明该产品将工作",然后你测试并运送它.
假设你有一个疯狂的叔叔jimbo,有一天,他把所有的工具拿出来,一堆两个四肢,并且工作了几个星期,在你的后院做了一个临时的过山车.然后你运行它,果然它不会崩溃.你甚至跑了十次,它不会崩溃.现在jimbo不是工程师,所以这不是根据标准制定的.但如果它在十次之后没有崩溃,这意味着它是安全的,你可以开始向公众收费,对吧?
什么是安全的,什么不是安全的,这在很大程度上是一个社会学问题.但是如果你想让它成为一个简单的问题:"我什么时候可以合理地假设没有人会受到我收费的伤害,当我无法对产品做任何假设"时,我就是这样做的.假设我估计,如果我开始向公众收费,我会用它运行X年,在那个时候,可能会有10万人骑它.如果它基本上是一个有偏见的硬币翻转,无论它是否破裂,那么我想要看到的是"这个设备已经运行了一百万次与碰撞假人,它从未坠毁或显示出破坏的暗示." 然后我可以合理地相信,如果我开始向公众收费,任何人受伤的可能性都很低,即使没有严格的工程标准.这只是基于统计和力学的一般知识.
关于你的问题,我想说,如果你发送的代码有未定义的行为,没有人,无论是标准,编译器制造商还是其他任何人都支持,那基本上是"疯狂的叔叔jimbo"工程,它只是"好的"如果你根据对统计数据和计算机的一般知识进行大量增加的测试来验证它是否满足你的需求.
你所指的是更有可能实现定义而不是未定义的行为.前者是标准没有告诉你会发生什么,但如果你使用相同的编译器和相同的平台它应该工作相同.一个例子是假设a int是4个字节长.UB更严重.那里的标准没有说什么.对于给定的编译器和平台,它可能有效,但它也可能仅在某些情况下有效.
一个例子是使用未初始化的值.如果你bool在a中使用未初始化if,你可能会得到真或假,并且可能会发生它总是你想要的,但代码会以几种令人惊讶的方式破解.
另一个例子是解除引用空指针.虽然它可能会在所有情况下导致段错误,但标准不要求程序甚至在每次运行程序时都产生相同的结果.
总而言之,如果您正在执行实现定义的操作,那么如果您只是开发到一个平台并且您测试它是否有效,那么您是安全的.如果您正在做一些未定义的行为,那么在任何情况下您都可能不安全.它可能有效,但没有任何保证.
以不同的方式思考它.
未定义的行为总是很糟糕,永远不应该使用,因为你永远不会知道你会得到什么.
但是,你可以用它来缓和
行为可以由除语言规范之外的其他方定义
因此,您永远不应该依赖UB,但是您可以找到备用源,这些源声明在您的环境中某个行为是编译器的DEFINED行为.
Yakk提供了关于快速代表类的很好的例子.根据规范,在这些情况下,作者明确声称他们正在从事未定义的行为.然而,他们接着解释了为什么行为比这更好地定义的商业原因.例如,他们声明成员函数指针的内存布局在Visual Studio中不太可能发生变化,因为由于不兼容而导致的商业成本猖獗,这对微软来说是令人不快的.因此,他们宣称行为是"事实上定义的行为".
在pthreads的典型linux实现中可以看到类似的行为(由gcc编译).在某些情况下,他们会假设允许编译器在多线程方案中调用哪些优化.这些假设在源代码的注释中明确说明.这是"事实上定义的行为?" 好吧,pthreads和gcc携手并进.将优化添加到打破pthreads的gcc会被认为是不可接受的,因此没有人会这样做.
但是,你不能做出同样的假设.你可以说"pthreads做到了,所以我也应该能够做到." 然后,有人进行优化,并更新gcc以使用它(可能使用__sync调用而不是依赖volatile).现在pthreads一直在运行......但是你的代码已不复存在了.
还要考虑MySQL的情况(或者它是Postgre?),他们发现了缓冲区溢出错误.实际上已经在代码中捕获了溢出,但它使用了未定义的行为,因此最新的gcc开始优化整个检出.
因此,总而言之,寻找定义行为的替代来源,而不是在未定义时使用它.找到你知道1.0/0.0等于NaN的原因是完全合法的,而不是导致浮点陷阱发生.但是,如果没有首先证明它是您和编译器的行为的有效定义,就不要使用该假设.
请哦,请记住我们时不时地升级编译器.