Arn*_*tta 32 c undefined-behavior language-lawyer
a = a++;
Run Code Online (Sandbox Code Playgroud)
在C中是不确定的行为.我问的问题是:为什么?
我的意思是,我认为可能很难提供一致的顺序来完成任务.但是,某些编译器总是在一个订单或另一个订单(在给定的优化级别)执行此操作.那么为什么要由编译器决定呢?
要清楚,我想知道这是否是一个设计决定,如果是,是什么促使它?或者可能存在某种硬件限制?
(注意:如果问题标题看起来不清楚或不够好,那么欢迎提供反馈和/或更改)
Eri*_*ert 47
更新:这个问题是我的博客2012年6月18日的主题.谢谢你这个好问题!
为什么?我想知道这是否是一个设计决定,如果是,是什么促使它?
你基本上要求ANSI C设计委员会会议记录,我没有那些方便.如果您的问题只能由那天在房间里的人明确回答,那么您将不得不找到那个房间里的人.
但是,我可以回答一个更广泛的问题:
导致语言设计委员会离开法律程序行为的一些因素是什么(
*)"未定义"或"实现定义"(**)?
第一个主要因素是:市场中是否存在两种语言的实现,这些实现不同意特定程序的行为?如果FooCorp的编译器编译M(A(), B())为"调用A,调用B,调用M",并且BarCorp的编译器将其编译为"调用B,调用A,调用M",并且两者都不是"明显正确"的行为,那么对该语言的强烈激励设计委员会说"你们都是对的",并使其实现定义的行为.特别是如果FooCorp和BarCorp都有委员会代表的情况.
下一个主要因素是:该功能是否自然地提供了许多不同的实施可能性?例如,在C#中,编译器对"查询理解"表达式的分析被指定为"对没有查询理解的等效程序进行语法转换,然后正常分析该程序".实现其他方面的自由度很低.
相比之下,C#规范说foreach环路应该被视为块while内的等效循环try,但允许实现一些灵活性.允许AC#编译器说,例如"我知道如何foreach在数组上更有效地实现循环语义"并使用数组的索引功能,而不是像规范建议的那样将数组转换为序列.
第三个因素是:特征是如此复杂,以至于指定其确切行为的详细细分将是困难或昂贵的?C#规范确实很少说明如何实现匿名方法,lambda表达式,表达式树,动态调用,迭代器块和异步块; 它只描述了所需的语义和对行为的一些限制,并将其余部分留给了实现.
第四个因素是:该功能是否会给编译器带来很大的负担?例如,在C#中,如果你有:
Func<int, int> f1 = (int x)=>x + 1;
Func<int, int> f2 = (int x)=>x + 1;
bool b = object.ReferenceEquals(f1, f2);
Run Code Online (Sandbox Code Playgroud)
假设我们要求b为真.你如何确定两个函数何时"相同"?进行"内涵"分析 - 函数体是否具有相同的内容? - 很难,并进行"扩展性"分析 - 当给出相同的输入时,这些函数是否具有相同的结果? - 更难.语言规范委员会应该尽量减少实施团队必须解决的开放研究问题的数量!
因此,在C#中,这是实现定义的; 编译器可以根据自己的判断选择使它们等于或不等.
第五个因素是:该功能是否会对运行时环境造成很大负担?
例如,在C#解除引用之后,数组的末尾是明确定义的; 它产生一个array-index-was-of-bounds异常.此功能可以在运行时以较小的实现而非零但成本较低的方式实现.使用null接收器调用实例或虚方法被定义为产生null-was-dereferenced异常; 再次,这可以用很小但非零的成本来实现.消除未定义行为的好处是支付较小的运行时成本.
第六个因素是:确定行为定义是否排除了一些主要的优化?例如,C#定义了从导致副作用的线程观察到的副作用的排序.但是,除了一些"特殊"的副作用之外,从另一个线程观察一个线程的副作用的程序的行为是实现定义的.(就像一个易失性写入,或进入一个锁.)如果C#语言要求所有线程以相同的顺序观察相同的副作用,那么我们将不得不限制现代处理器有效地完成他们的工作; 现代处理器依赖于无序执行和复杂的缓存策略来获得高水平的性能.
这些只是我想到的几个因素; 当然,在制作"实现定义"或"未定义"特征之前,语言设计委员会会讨论许多其他因素.
现在让我们回到您的具体示例.
C#语言确实严格定义了该行为(†); 观察到增量的副作用发生在赋值的副作用之前.因此,那里不可能存在任何"好,它只是不可能"的论点,因为可以选择行为并坚持下去.这也不妨碍优化的主要机会.并且没有多种可能的复杂实施策略.
因此,我的猜测,并且我强调这是一个猜测,是C语言委员会将副作用排序到实现定义的行为中,因为市场中有多个编译器以不同方式执行,没有一个明显"更正确",委员会不愿意告诉他们一半他们错了.
(*)或者,有时,它的编译器!但是让我们忽略那个因素.
(**)"未定义"行为意味着代码可以执行任何操作,包括擦除硬盘.编译器不需要生成具有任何特定行为的代码,也不需要告诉您它正在生成具有未定义行为的代码."实现定义"行为意味着编译器作者在选择实现策略时有相当大的自由度,但需要选择策略,一致地使用它并记录该选择.
(†)当然,从单个线程观察时.
小智 11
它是未定义的,因为编写这样的代码没有充分的理由,并且不需要伪代码的任何特定行为,编译器可以更积极地优化编写良好的代码.例如,*p = i++可能会以导致崩溃的方式进行优化(如果p恰好指向)i,可能是因为两个内核同时写入同一内存位置.这也恰好在特定的情况下是不确定的,这一事实*p被明确写出来的i,得到i = i++的,逻辑如下.
除了少数例外,表达式的评估顺序是未指定的; 这是一个深思熟虑的设计决策,它允许实现从编写的内容重新排列评估顺序,如果这将导致更高效的机器代码.类似地,除了在下一个序列点之前发生的要求之外,未指定其副作用++和--应用的顺序,再次为实现提供以最佳方式排列操作的自由度.
不幸的是,这意味着表达式的结果a = a++会因编译器,编译器设置,周围代码等而异.行为在语言标准中被特别称为未定义,因此编译器实现者不必担心检测此类案件并对其进行诊断.像这样a = a++的案例是显而易见的,但是类似的事情呢
void foo(int *a, int *b)
{
*a = (*b)++;
}
Run Code Online (Sandbox Code Playgroud)
如果是这样的文件中的唯一功能(或者如果它的调用者在不同的文件),也没有办法知道在编译时是否a和b指向同一个对象; 你是做什么?
请注意,完全可以强制要求所有表达式按特定顺序进行评估,并且所有副作用都应用于评估中的特定点; 这就是Java和C#所做的,在那些语言中,表达式a = a++总是很明确.