未定义的行为值得吗?

时间:2010-05-05 09:11:07

标签: c++ undefined-behavior

由于未定义的行为,许多不好的事情发生并且继续发生(或者不知道,谁知道,任何事情都可能发生)。据我所知,这是为了让编译器进行优化留下一些摆动空间,也可能使C ++更容易移植到不同的平台和架构。然而,由未定义的行为引起的问题似乎太大而无法通过这些论证来证明。未定义行为的其他参数是什么?如果没有,为什么还存在未定义的行为?

编辑为我的问题添加一些动力:由于使用较少C ++的几个不好的经历 - 狡猾的同事,我已经习惯了使我的代码尽可能安全。断言每一个论点,严谨的正确性以及类似的东西。我试图离开,因为小房间可能以错误的方式使用我的代码,因为经验表明,如果有漏洞,人们会使用它们,然后他们会打电话给我关于我的代码是坏的。我认为让我的代码尽可能安全是一种好的做法。这就是为什么我不明白为什么存在未定义的行为。有人可以给我一个未定义行为的例子,这些行为在运行时或编译时无法检测到,而没有相当大的开销吗?

11 个答案:

答案 0 :(得分:9)

我对未定义行为的看法是:

该标准定义了语言的使用方式,以及在以正确方式使用时应如何做出反应。但是,要涵盖每个功能的每个可能用途都需要做很多工作,所以标准就是这样做。

但是,在编译器实现中,您不能只是“保留它”,代码必须转换为机器指令,并且您不能只留下空白点。在许多情况下,编译器可能会抛出错误,但这并不总是可行的:在某些情况下需要额外的工作来检查程序员是否做错了(例如:调用析构函数两次 - 来检测这个,编译器必须计算已调用某些函数的次数,或添加额外的状态或其他内容)。因此,如果标准没有定义它,并且编译器只是让它发生,那么有时可能会发生诙谐的事情,如果你不幸的话。

答案 1 :(得分:8)

我认为关注的核心来自于C / C ++的速度哲学。

这些语言是在原始电源稀疏的情况下创建的,您需要获得所有优化才能获得可用的功能。

指定如何处理UB意味着首先检测它,然后当然指定正确的处理。然而,检测它是违反语言的速度第一哲学!

今天,我们还需要快速的节目吗?是的,对于那些使用非常有限的资源(嵌入式系统)或非常严格的约束(响应时间或每秒事务数)工作的人,我们确实需要尽可能地挤出来。

我知道座右铭在问题上投入更多硬件。我有一个申请工作:

  • 答案的预计时间?不到100毫秒,中间有数据库调用(感谢memcached)。
  • 每秒的交易次数?平均1200,峰值1500/1700。

它运行在大约40个怪物:8个双核opteron(2800MHz)和32GB RAM。此时使用更多硬件变得“更快”变得困难,因此我们需要优化代码和允许它的语言(我们确实限制在那里抛出汇编代码)。

我必须说,无论如何我对UB并不在意。如果你达到程序调用UB的程度那么它需要修复实际发生的任何行为。当然,如果立即报告它们会更容易修复它们:这就是调试构建的目的。

所以也许我们不应该专注于UB,而应该学会使用这种语言:

  • 不要使用未经检查的电话
  • (对于专家)不要使用未经检查的电话
  • (对于大师)你确定你真的需要一个未经检查的电话吗?

一切都突然好转了。)

答案 2 :(得分:6)

问题不是由未定义的行为引起的,它们是由编写导致它的代码引起的。答案很简单 - 不要写那种代码 - 不这样做并不完全是火箭科学。

至于:

  

未定义行为的示例   无法在运行时检测到   编译时间不大   开销

现实问题:

int * p = new int;
// call loads of stuff which may create an alias to p called q
delete p;

// call more stuff, somewhere in which you do:
delete q;

在编译时检测到这一点是不可能的。在运行时,它只是非常困难,并且需要内存分配系统进行更多的簿记(即更慢并占用更多内存),而不是简单地说第二次删除是未定义的。如果你不喜欢这个,也许C ++不是你的语言 - 为什么不切换到java?

答案 3 :(得分:5)

未定义行为的主要来源是指针,这就是C和C ++有很多未定义行为的原因。

考虑以下代码:

char * r = 0x012345ff;
std::cout << r;

此代码看起来非常糟糕,但它应该发出错误吗?如果该地址确实可读,即它是我以某种方式获得的值(可能是设备地址等)怎么办?

在这种情况下,无法知道操作是否合法,如果不合法,则其行为确实无法预测。

除此之外:一般来说C ++的设计考虑了“零开销规则”(参见The Design and Evolution of C++),因此它不可能对实施检查角落情况等施加任何负担。你应该请记住,这种语言的设计不仅适用于桌面,也适用于资源有限的嵌入式系统。

答案 4 :(得分:4)

如果不是不可能通过编译器或运行时环境进行诊断,很多被定义为未定义行为的事情都很难。

那些容易的已经变成定义的 - 未定义的行为。考虑调用纯虚方法:它是未定义的行为,但大多数编译器/运行时环境将以相同的术语提供错误:纯虚方法称为。事实上的标准是调用纯虚方法调用是我所知道的所有环境中的运行时错误。

答案 5 :(得分:3)

标准保留了“某些”行为未定义,以便允许各种实现,而不会增加这些实现的负担,检测“某些”情况,或者给程序员带来必要的约束,以防止首先出现这些情况

有一段时间,避免这种开销是C和C ++在大量项目中的主要优势。

计算机现在比C发明时快了几千倍,并且像检查数组边界一样,或者有几兆字节的代码来实现沙盒运行时的开销看起来不像是大多数项目都很重要。此外,由于我们的程序每秒处理数兆字节的潜在恶意数据,因此(例如)超越缓冲区的成本增加了几个因素。

因此,有些语言具有C ++的所有有用功能,并且还具有定义编译的每个程序的行为(受特定于实现的行为)的特性,这有点令人沮丧。但只是在某种程度上 - 在Java中编写行为非常混乱的代码实际上并不困难,因为从调试的POV来看,如果不是安全性,它可能也是未定义的。编写不安全的Java代码也一点都不困难 - 只是不安全性通常仅限于泄漏敏感信息或授予应用程序不正确的权限,而不是完全控制JVM运行的操作系统进程。

所以我认为好的软件工程需要所有语言的学科,不同之处在于当我们的学科失败时会发生什么,以及我们用其他语言收取多少费用(性能和足迹以及你喜欢的C ++功能) )保险。如果由其他语言提供的保险对您的项目是值得的,请接受它。如果C ++提供的功能值得付出以及未定义行为的风险,请使用C ++。我不认为试图争论有多少里程,好像它是一个对每个人都一样的全球财产,无论C ++的好处是否“证明”成本。它们在C ++语言设计的参考条款中是合理的,这是你不支付你不使用的。因此,正确的程序不应该变慢,以便不正确的程序获得有用的错误消息而不是UB,并且不应该定义异常情况下的大部分时间行为(例如,32位值的<< 32) (例如,导致0)如果这需要在委员会希望“有效”支持C ++的硬件上明确检查异常情况。

再看另一个例子:我不认为英特尔专业C和C ++编译器的性能优势可以证明购买它的成本是合理的。因此,我没有买它。并不意味着其他人会做出我所做的相同计算,或者我将来也会做同样的计算。

答案 6 :(得分:2)

编译器和编程语言是我最喜欢的主题之一。在过去,我做了一些与编译器有关的研究,我发现很多次未定义的行为

C ++和Java非常受欢迎。这并不意味着他们有一个伟大的设计。它们被广泛使用,因为它们冒险而不考虑其设计质量只是为了获得认可。 Java用于垃圾收集,虚拟机和无指针外观。他们是部分先驱,无法从以前的许多项目中学习。

对于C ++,其中一个主要目标是为C用户提供面向对象的编程。甚至C程序也应该使用C ++编译器进行编译。这造成了许多令人讨厌的开放点,C已经有很多含糊之处。 C ++重点是权力和普及,而不是诚信。没有多少语言可以为您提供多重继承,C ++会为您提供尽管不是非常精致的方式。未定义的行为总是会支持它的荣耀和向后兼容性。

如果你真的想要一个强大且定义明确的语言,你必须寻找其他地方。可悲的是,这不是大多数人的主要关注点。例如,Ada是一种很好的语言,其中明确且明确的行为很重要,但由于其用户群较窄,几乎没有人关心该语言。我对这个例子有偏见,因为我非常喜欢这种语言,我发布了一些on my blog但是如果你想了解更多关于语言定义如何帮助在编译之前减少错误的问题,请查看{{} 3}}

我不是说C ++是一种糟糕的语言!它只是有不同的目标,我喜欢与它合作。您还拥有一个庞大的社区,出色的工具以及更多优秀的东西,如STL,Boost和QT。但是你的疑问也是成为一名出色的C ++程序员的根本。如果你想要很好地使用C ++,这应该是你关心的问题之一。我建议您阅读之前的幻灯片以及these slides。当语言没有达到你期望的效果时,它将帮助你理解那些时候。

顺便说一下。未定义的行为完全违背了可移植性。例如,在Ada中,您可以控制数据结构的布局(在C和C ++中,它可以根据机器和编译器进行更改)。线程是语言的一部分。所以移植C和C ++软件会给你带来更多痛苦而不是快乐

答案 7 :(得分:2)

明确未定义行为与实现定义行为之间的差异非常重要。实现定义的行为为编译器编写者提供了添加语言扩展的机会,以便利用他们的平台。这些扩展对于编写在现实世界中有效的代码是必要的。

另一方面,UB存在于难以或不可能设计解决方案而不对语言进行重大更改或与C存在较大差异的情况。从page where BS talks about this获取的一个示例是:

int a[10];
a[100] = 0; // range error
int* p = a;
// ...
p[100] = 0; // range error (unless we gave p a better value before that assignment)

范围错误是UB。这是一个错误,但是标准未定义平台应该如何精确处理这个问题,因为标准无法定义它。每个平台都不同。它无法设计为错误,因为这需要在语言中包含自动范围检查,这需要对语言的功能集进行重大更改。语法在编译或运行时生成诊断更加困难p[100] = 0错误,因为编译器无法在没有运行时支持的情况下知道p真正指向的内容

答案 8 :(得分:1)

几年前,我问自己同样的问题。当我试图为写入空指针的函数的行为提供正确的定义时,我立即停止考虑它。

并非所有设备都具有受保护内存的概念。因此,您不可能依靠系统来通过段错误或类似方法来保护您。并非所有设备都具有只读存储器,因此您不可能说写操作无效。我能想到的唯一另一个选择是要求应用程序在没有系统帮助的情况下引发异常[或中止或其他]。但在这种情况下,编译器必须在每次单个内存写入之前插入代码以检查null,除非它可以保证指针自列表内存写入后没有更改。这显然是不可接受的。

因此,保留未定义的行为是我唯一可以做出的逻辑决定,并没有说“合规C ++编译器只能在具有受保护内存的平台上实现。”

答案 9 :(得分:1)

这是我的最爱:在使用它(不仅是解除引用,还有castin等)的非空指针上完成delete之后是UB(参见this question)

如何进入UB:

{
    char* pointer = new char[10];
    delete[] pointer;
    // some other code
    printf( "deleted %x\n", pointer );
}

现在在所有架构上我都知道上面的代码运行正常。教授编译器或运行时以执行对这种情况的分析是非常困难和昂贵的。不要忘记有时在delete和使用指针之间可能有数百万行代码。在delete之后立即将设置指针设置为null可能成本很高,因此它也不是通用的解决方案。

这就是为什么有UB的概念。您不希望代码中包含UB。也许工作可能没有。适用于此实现,打破另一个。

答案 10 :(得分:0)

有时候未定义的行为是好的。以一个大的int为例。

union BitInt
{
    __int64 Whole;
    struct
    {
        int Upper;
        int Lower; // or maybe it's lower upper. Depends on architecture
    } Parts;
};

规范说如果我们上次阅读或写入Whole,那么部件的读/写是未定义的。

现在,这对我来说只是一点点愚蠢,因为如果我们无法触及工会的任何其他部分,那么首先要有工会是没有意义的,对吗?

但无论如何,也许某些函数将采用__int64而其他函数采用两个独立的整数。而不是每次我们可以使用这个联合转换。我知道的每个编译器都以非常清晰的方式处理这个未定义的行为。所以在我看来,未定义的行为在这里并不是那么糟糕。