我有一个类,其中我在特定成员上使用 memcmp()
重载了 ==
运算符。由于在代码中完成了错误的复制(memcpy
以比应有的更大的大小调用),我在调用 ==
运算符时遇到了段错误。
我知道 UB 是神秘的,而且显然是未定义的,但仍然有一些我注意到的东西引起了我的兴趣。
在调试时,我将 ==
调用与其实现交换(即 a==b
与 memcmp(a.member_x, b. member_x, SIZE)
) 并且没有段错误!
那么,使用运算符本身和用实现替换它之间有区别吗?还是这只是 UB?
澄清一下:是的,此代码包含 UB。这很糟糕,它的结果是不确定的。我想知道的是:调用操作符或调用它的主体时会发生什么不同吗?UB 只是让我觉得可能存在差异(并且显然已修复)
最佳答案
未定义行为意味着“任何事情都可能发生”。 “任何事情”包括“按预期工作”。这可能意味着您可以在不更改任何内容的情况下获得不同的行为,也可能意味着即使您进行了更改也可以获得相同的行为。
过去,关于依赖未定义行为的警告通常包括众所周知的“发射核导弹”。
但是,使用现代积极优化的编译器,行为可能会更加微妙。在过去,未定义的行为通常会导致“发生什么事就发生什么”。例如。在您的示例中,如果您被允许访问它,您将在内存中读取“垃圾”,如果不允许,您将读取段错误。但是操作(即“比较这两个内存块”)仍然会以某种方式发生。
现代积极优化的编译器不再“保证”(在 UB 方面,没有任何保证)。编译器将不再只是做无意义的事情。
对于现代优化编译器,编译器必须经常决定(或证明)某种优化是安全的,即它不会改变可观察到的指定行为。由于 UB 的意思是“任何事情都可能发生”,这意味着证明某些优化是安全的优化器部分可以“假设任何事情”。本质上,它可以假设所有优化都是安全的,然后继续进行它想要提供最积极的优化。
因此,UB 比以前更难预测,也更不明显。例如,UB 在程序的一个地方可以导致优化器以某种方式优化某些东西,它改变以某种方式连接到这段代码的程序的不同部分中其他东西的行为(例如它调用它,或者两者都操纵相同的状态)。
假设我们有两个线程在处理共享的可变状态。两个线程之一显示 UB。然后,优化器可以决定该线程不操纵状态(“任何事情都可能发生”,还记得吗?)并且因为它现在可以证明该状态只会被一个线程访问,它可以优化掉所有的锁! [注意:我不知道现实中是否有任何编译器会这样做,但它会被允许这样做!]
这是另一个例子来证明“任何事情都可能发生”真的,真的确实意味着“任何事情”:让我们假设有两种可能的优化可以应用在调用堆栈更高层的代码中你的 operator==
。一种优化只有在编译器可以证明 operator==
永远为真时才有效。另一个优化只有在编译器可以证明它永远为假时才有效。这当然意味着,这两种优化都不能应用,因为一般来说,您的 operator==
可能返回 true 或 false。
但是!我们有 UB。因此,编译器可以决定假设它始终为真并应用优化 #1。或者它可以决定它始终为假并应用优化#2。好吧,很公平。但是,它也可以决定应用两种优化!请记住:“任何事情都可能发生”。不仅仅是“根据 C++ 规范的逻辑框架有意义的任何东西”,而是“任何”时期。如果编译器需要某些东西同时为真和假,它可以在 UB 存在的情况下自由假设。
您可以将现代优化编译器视为尝试证明关于您的代码的定理,然后根据这些证明应用优化。 UB 允许它证明任何和所有定理。
关于C++:调用运算符和调用它的实现之间有区别吗,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/44879921/