如果未将“ i = i ++”视为未定义行为,将会发生什么?

时间:2019-01-11 14:01:07

标签: c language-lawyer

我无法理解未指定行为和未定义行为之间的区别。我认为尝试理解一些示例会很有用。例如,20181231,USD,USD,1.0000 20181231,USD,AUD,1.4220 20181231,USD,BRL,3.8828 20181231,USD,GBP,0.7880 20181231,USD,CAD,1.3643 20181231,USD,CNY,6.8782 20181231,USD,DKK,6.5295 20181231,USD,HKD,7.8316 20181231,USD,INR,69.7304 20181231,USD,IDR,14565.1115 20181231,USD,ILS,3.7693 20181231,USD,JPY,110.3364 20181231,USD,MXN,19.6687 20181231,USD,NOK,8.7288 20181231,USD,PHP,52.7242 20181231,USD,PLN,3.7660 20181231,USD,SGD,1.3674 20181231,USD,SKK,26.3387 20181231,USD,ZAR,14.4448 20181231,USD,KRW,1117.0950 20181231,USD,SEK,8.9869 20181231,USD,CHF,0.9846 20181231,USD,TWD,30.6213 20181231,USD,THB,32.6870 20181231,USD,EUR,0.8743 20181231,USD,MYR,4.1555 20181231,USD,NZD,1.4927 20181231,USD,SAR,3.7565 20181231,USD,TRY,5.2892 20181231,USD,RUB,69.4108 20181231,USD,CZK,22.5409 20181231,USD,AED,3.6728 20181231,USD,CLP,694.7031 20181231,USD,EGP,17.9262 20181231,USD,MAD,9.5636 20181231,USD,NGN,365.4074 20181231,USD,OMR,0.3860 20181231,USD,QAR,3.6423 。此分配的问题是:

  

在上一个和下一个序列点之间,对象应具有其存储值   通过对表达式的求值最多只能修改一次。此外,在先值只能是只读的,以确定要存储的值。

这违反了准则,但未明确调用未定义的行为,但根据以下内容涉及UB:

  

操作数的评估顺序未指定。如果试图修改赋值运算符的结果或在下一个序列点之后访问它,则行为是不确定的。

假设这些规则都不存在,并且没有其他规则会使“ x = x++”无效。那么x = x++的值将是未指定的,对吧?

之所以引起怀疑,是因为有时有人争辩说C中的东西是“默认”为UB才是有效的,您可以证明构造是正确的。

编辑:如P.W所指出,对于C ++,存在一个与此相关的,易于理解的版本:What made i = i++ + 1; legal in C++17?

5 个答案:

答案 0 :(得分:3)

  

我无法理解未指定行为和未定义行为之间的区别。

然后让我们从标准中这些术语的定义开始:


  

未定义行为行为,在使用不可移植或错误的程序构造或错误的数据时,为此   国际标准没有任何要求

     

注意可能的不确定行为范围从完全忽略具有无法预测结果的情况到在运行过程中的行为   以书面形式进行翻译或程序执行的特征   环境的情况(有无诊断信息发布   消息),以终止翻译或执行(与签发   诊断消息)。

     

示例未定义行为的一个示例是整数溢出时的行为。

C2011, 3.4.3


  

未指定行为使用未指定的值,或本国际标准提供两个或两个以上的其他行为   可能性,对选择哪个对象没有任何进一步的要求   在任何情况下

     

示例未指定行为的一个示例是   对函数的参数进行求值。

C2011, 3.4.4


您说的是

  

之所以引起怀疑,是因为有时有人认为C中的东西是   UB“默认”仅有效,您可以证明该构造   有效。

称它为论点也许过于夸张,好像对其有效性存有疑问。实际上,它反映了标准中的显式语言:

  

如果“要求”或“不得”要求出现在   约束或运行时约束被违反,行为是   未定义。 未定义的行为在此表示   国际标准 ,用“未定义的行为”或   省略任何明确的行为定义 。没有   这三个重点之间的差异;他们都描述``行为   那是不确定的''。

({C2011, 4/2;已添加重点)

当你摆姿势

  

假设这些规则都不存在,并且没有其他规则可以   “无效” x = x ++。

,并不一定会改变任何内容。特别是,删除未指定操作数评估顺序的显式规则不会使顺序成为指定。我倾向于争辩说顺序仍然不确定,但是另一种选择是行为是不确定的。明确表示未指定的主要目的是避开该问题。

当在序列点之间两次修改对象时明确声明UB的规则不太清楚,但是落在同一条船上。有人可能会争辩说,该标准仍然没有为您的示例案例定义行为,从而使其未定义。我认为这有点儿麻烦,但这就是为什么有一个明确的规则,一种方式或另一种方式很有用的原因。可以针对您的情况定义行为,例如Java可以,但是出于各种技术和历史原因,C选择不这样做。

  

然后x的值将不确定,对吧?

这还不是很清楚。

也请理解,该标准的各种规定在大多数情况下并不孤立。它们被设计为作为一个(大部分)连贯的整体一起工作。删除或更改随机条款具有产生不一致或漏洞的巨大风险,因此很难对结果进行推理。

答案 1 :(得分:2)

现代C11 / C17更改了文本,但含义几乎相同。 C17 6.5 / 2:

  

如果相对于相同标量对象上的不同副作用或使用相同标量值的值计算,相对于标量对象的副作用未排序   对象,其行为是不确定的。

这里有几个稍微不同的问题,混在一起:

  • 在序列点之间,x被多次写入(副作用)。这是上述的UB。
  • 在序列点之间,表达式包含至少一个副作用,并且存在与不存储哪个值无关的相同变量的值计算。如上所述,这也是UB。
  • 在表达式x = x++中,操作数x的求值没有相对于操作数x++进行排序。根据C17 6.5.16,评估顺序是未指定的行为。

      

    更新左操作数的存储值的副作用是   在左右操作数的值计算之后排序。的评价   操作数是无序列的。

如果没有为标记该UB的第一个引用部分,那么我们仍然不知道x++是在对左x操作数求值之前还是之后进行排序,因此很难推断这将如何变成“只是未指明的行为”。

C ++ 17实际上修复了此部分,使其在此处得到了很好的定义,这与C或更早的C ++版本不同。他们通过定义序列顺序(C ++ 17 8.5.18)来做到这一点:

  

在所有情况下,赋值都在值之后排序   左右操作数的计算,以及赋值表达式的值计算之前。   右操作数在左操作数之前排序。

我看不到这里有什么中间立场;表达式未定义或定义良好。


未指定的行为是确定性行为,我们无法了解或假设。但是与未定义的行为不同,它不会导致崩溃和随机程序行为。 a() + b()是一个很好的例子。我们不知道将首先执行哪个函数-如果同一行的后面出现同一行,则该程序甚至不必保持一致。但是我们知道这两个函数将被执行,一个在另一个上。

x = a() + b() + x++;不同,["itemA","itemAA","itemAa"] : 10 是未定义的行为,我们无法承担任何责任。可以以任何顺序执行一个功能,两个功能或不执行任何功能。程序可能崩溃,产生不正确的结果,产生看似正确的结果,或者什么也不做。

答案 2 :(得分:2)

在其他编程语言中,有一些实例在后来的标准中定义了以前未定义的行为。我记得的一个例子是在C ++中,C ++ 11中未定义的行为在C ++ 17中得到了很好的定义。

i = i++ + 1; // the behavior is undefined in C++11 

i = i++ + 1; // the behavior is well-defined in C++17. The value of i is incremented

关于这个主题有well received question。 定义得当的原因是C ++ 17标准中的保证

  

右操作数在左操作数之前排序。

因此,从某种意义上说,由标准委员会成员来更改标准并提供强有力的保证以使它定义明确。

但是我不认为像x = x++;这样简单的事情不会被指定。它会是未定义的或定义良好的。

答案 3 :(得分:2)

问题似乎是无法正确定义i= i++;的含义:

解释1:

    int i1= i;
    int i2= i1+1;
    i = i2;
    i = i1;

在这种解释中,检索i的值并添加1(i2),然后将此i2保存到i,但是原始的{{1} i中的}还会在赋值中使用(因为这里i1被解释为在使用后才应用于该值),因此++不变。

解释2:

i

在这种解释中,首先执行 int i1= i; i1= i1+1; i= i1; int i2= i; i= i2; (并修改i++),然后再次检索修改后的i并将其用于分配中(因此i具有增值)。

解释3:

i

在这种解释中,首先执行 int i1= i; i = i1; int i2= i1+1; i= i2; i的分配,然后将i递增。

对我来说,所有这三种解释都是正确的,甚至可能还会有更多的解释,但是它们各自做的事情有所不同。因此,该标准不能/没有定义它,并且编译器使用哪种解释取决于编译器生成器,结果,编译器呈现的行为是不确定的:未定义的行为。

(编译器甚至可以生成i指令或忽略整个语句。)

答案 4 :(得分:1)

++的副作用的评估和应用顺序为未指定-语言标准不强制要求从左至右或从右至左的顺序(对于算术运算符)。考虑定义明确的表达式a = b++ * ++c。表达式ab++++c可以按任何顺序求值。类似地,对bc的副作用可以在评估后立即应用,或推迟到下一个序列点之前或之间的任何位置。重要的是b * (c+1)的结果是在分配给a之前计算的。以下是一项完美的法律评估:

tmp <- c + 1;
a = b * tmp;
c <- c + 1
b <- b + 1

这是

c <- c + 1
a <- b * c
b <- b + 1

这是

tmp1 <- b
b <- b + 1
tmp2 <- c + 1
a <- tmp1 * tmp2
c <- c + 1

重要的是,无论选择哪种评估顺序,您都将始终获得相同的结果。

x = x++可以通过以下两种方式进行评估,具体取决于应用副作用的时间:

Option 1         Option 2
--------         --------
tmp <- x         tmp <- x
x <- x + 1       x <- tmp
x <- tmp         x <- x + 1

问题在于这两种方法给出的结果不同。根据给出不同于这两种结果的指令集,可以使用其他完全不同的方法。

当表达式根据求值顺序给出不同的结果时,语言标准没有强制要求执行该操作-它不对编译器或运行时环境提出任何要求以选择任何一个选项。这就是 undefined 的含义-实际上,语言规范并未定义行为。您会得到 a 结果,但不能保证结果一致,也不会保证结果符合预期。

未定义并不表示非法。这也不意味着您的代码肯定会崩溃。这仅表示结果不可预测或无法保证是一致的。一个实现甚至不必发出诊断说“嘿,假人,这是个坏主意。”

实现可以自由定义和记录标准未定义的行为(例如MSVC在输入流上定义fflush)。许多编译器利用未定义的某些行为来执行一些优化。而且有些编译器会针对诸如x = x++之类的常见错误发出警告。