RE2中未明确的行为,表明定义明确

时间:2017-12-05 12:23:35

标签: c++ language-lawyer valgrind undefined-behavior re2

最近我发现RE2库使用this technique进行快速查找。在查找期间,它使用来自未初始化数组的值,据我所知,这是未定义的行为。

我甚至发现this issue有关于使用未初始化内存的valgrind警告。但是这个问题已被关闭,并且评论说这种行为已被取消。

我认为实际上未初始化的数组只会在所有现代编译器和体系结构中包含一些随机数据。但另一方面,我将“未定义行为”声明视为“字面上任何事情都可能发生”(包括您的程序格式化您的硬盘或哥斯拉来破坏您的城市)。

问题是:在C ++中使用未初始化的数据是否合法?

1 个答案:

答案 0 :(得分:0)

最初设计C时,如果arr是某个类型T占用N字节的数组,则arr[i]这样的表达式意味着“取{的基址{1}},向其添加arr,在结果地址获取i*N个字节,并将其解释为N“。如果T字节的每个可能组合在解释为类型N时都具有意义,则获取未初始化的数组元素可能会产生任意值,但行为本来是可预测的。如果T是32位类型,则尝试读取类型为T的未初始化数组元素将产生最多4294967296个可能行为中的一个;当且仅当这些行为中的每一个都符合计划的要求时,这样的行动才是安全的。如您所知,有些情况下这种保证很有用。

然而,C标准描述了一种语义较弱的语言,它不保证读取未初始化的数组元素的行为将以与存储可能包含的任何位模式一致的方式运行。编译器编写者希望处理这种较弱的语言,而不是Dennis Ritchie发明的语言,因为它允许他们应用一些优化而不考虑它们如何交互。例如,如果代码执行T并且稍后执行a=x;b=a;,并且编译器无法“看到”原始分配与可能更改的后分配之间的任何内容{{ 1}}或c=a;,它可以省略第一个分配,并将后两个分配更改为ax。但是,如果后两个分配之间发生了某些情况会发生变化b=x;,那么可能会导致c=x;x获得不同的值 - 如果没有任何变化,则应该是不可能的{{{ 1}}。

如果没有任何改变b,那么应用该优化本身不会成为问题。另一方面,考虑使用某些已分配存储作为类型c的代码,释放它,重新分配它,并将其用作类型a。如果编译器知道原始请求和替换请求的大小相同,则可以在不释放和重新分配存储的情况下回收存储。但是,这可能导致代码序列:

x

被重写为:

float

特别是在处理intfloat *fp = malloc(4); ... *fp = slowCalculation(); somethingElse = *fp; free(fp); int *ip = malloc(4); ... a=*ip; b=a; ... c=a; 的写入之间缓慢计算的结果时,性能很少受益。遗憾的是,编译器的设计方式并不能方便地保证延迟计算不会偶然降落在导致故障的地方。

就我个人而言,我认为编译器编写者的哲学是严重错误的:如果某个程序员在某种情况下知道保证会有用,那么要求程序员解决它的缺乏会带来很大的成本和100%的确定性。相比之下,编译器避免以缺乏该保证为基础的优化的要求很少会花费任何成本(因为解决其缺失的代码几乎肯定会阻止“优化”)。不幸的是,有些人似乎更感兴趣的是优化那些不需要超出标准要求的保证的源文本的性能,而不是优化编译器生成代码以完成有用任务的效率。