为什么(x + = x + = 1)在C和Javascript中的评价方式不同?

KT.*_*KT. 27 javascript c puzzle semantics

如果变量的值x最初为0,则表达式x += x += 1将在C中计算为2,在Javascript中计算为1.

C的语义对我来说似乎很明显:x += x += 1被解释为x += (x += 1)反过来相当于

x += 1
x += x  // where x is 1 at this point
Run Code Online (Sandbox Code Playgroud)

Javascript解释背后的逻辑是什么?什么规范强制执行这种行为?(顺便说一句,应该注意Java在这里与Javascript一致).

更新: 事实证明,x += x += 1根据C标准,表达式具有未定义的行为(感谢ouah,John Bode,DarkDust,Drew Dormann),这似乎破坏了一些读者的问题的全部要点.通过在其中插入标识函数,可以使表达式符合标准:x += id(x += 1).可以对Javascript代码进行相同的修改,问题仍然如所述.假设大多数读者能够理解"非标准兼容"制定背后的观点,我会保留它,因为它更简洁.

更新2:事实证明,根据C99,身份函数的引入可能无法解决模糊性.在这种情况下,亲爱的读者,请将原始问题视为与C++而不是C99有关,其中"+ ="现在可能最安全地被视为具有唯一定义的操作序列的可重载运算符.也就是说,x += x += 1现在相当于operator+=(x, operator+=(x, 1)).对于通向标准的漫长道路感到抱歉.

oua*_*uah 27

x += x += 1; 在C中是未定义的行为

表达式语句违反了序列点规则.

(C99,6.5p2)"在前一个和下一个序列点之间,一个对象的存储值最多只能通过表达式的评估来修改一次."

  • @KT:绝对是UB,如果"按预期"表示"按照KT的预期",它只表现为"按预期".在C语言的基础上,没有理由期望它有任何有意义的结果或不会崩溃您的计算机. (8认同)
  • "现在只是内联+ =而不是myop." 当您进行更改时,行为将变为未定义(或者,当使用`id`函数时,行为将变得未指定且可能未定义).函数调用具有比基本运算符评估更强的排序要求. (6认同)
  • @jpalecek这一个是陷阱.在函数调用中有一个序列点,这意味着所有副作用都在函数体执行之前发生,但是赋值运算符的操作数的评估顺序仍未指定. (4认同)
  • 即使它确实是未定义的行为(我认为,从所提出的段落中并不完全明显),我已经在几个C编译器中对此进行了测试,并且表达式的行为符合预期.因此,我的问题更多的是关于Javascript在这里做什么.同样,Chrome和Firefox解释器似乎都同意这种解释. (3认同)
  • 有人应该注意,这只是答案的一半. (3认同)

Mic*_*urr 15

JavaScript和Java对此表达式具有非常严格的从左到右的评估规则.C不会(即使在您提供的具有身份功能干预的版本中).

我有ECMAScript规范(第3版,我承认它已经很老了 - 目前的版本可以在这里找到:http://www.ecma-international.org/publications/files/ECMA-ST/Ecma-262. pdf)表示复合赋值运算符的计算如下:

11.13.2复合赋值(op =)

生产AssignmentExpression:LeftHandSideExpression @ = AssignmentExpression,其中@代表上面指出的一个运算符,评估如下:

  1. 评估LeftHandSideExpression.
  2. 调用GetValue(Result(1)).
  3. 评估AssignmentExpression.
  4. 调用GetValue(Result(3)).
  5. 将operator @应用于Result(2)和Result(4).
  6. 调用PutValue(结果(1),结果(5)).
  7. 返回结果(5)

您注意到Java具有与JavaScript相同的行为 - 我认为其规范更具可读性,因此我将在此处发布一些片段(http://java.sun.com/docs/books/jls/third_edition/html/expressions. html#15.7):

15.7评估订单

Java编程语言保证运算符的操作数似乎以特定的评估顺序进行评估,即从左到右.

建议代码不要严格依赖于此规范.当每个表达式最多包含一个副作用时,代码通常更清晰,作为其最外层的操作,并且当代码不依赖于由于从左到右的表达式评估而出现的确切异常时.

15.7.1首先评估左侧操作数在评估右侧操作数的任何部分之前,似乎完全评估了二元运算符的左侧操作数.例如,如果左侧操作数包含对变量的赋值而右侧操作数包含对该同一变量的引用,则引用生成的值将反映赋值首先发生的事实.

...

如果运算符是复合赋值运算符(第15.26.2节),则对左侧操作数的计算包括记住左侧操作数表示的变量并获取并保存该变量的值以用于隐含的组合操作.

另一方面,在提供中间身份函数的not-undefined-behavior示例中:

x += id(x += 1);
Run Code Online (Sandbox Code Playgroud)

虽然它不是未定义的行为(因为函数调用提供了一个序列点),但是x在函数调用之前或之后评估最左边的行为仍然是未指定的行为.因此,虽然它不是'任何事情'未定义的行为,但仍然允许C编译器x在调用id()函数之前评估这两个变量,在这种情况下,存储到变量的最终值将是1:

例如,如果x == 0要开始,评估可能如下所示:

tmp = x;    // tmp == 0
x = tmp  +  id( x = tmp + 1)
// x == 1 at this point
Run Code Online (Sandbox Code Playgroud)

或者它可以这样评价:

tmp = id( x = x + 1);   // tmp == 1, x == 1
x = x + tmp;
// x == 2 at this point
Run Code Online (Sandbox Code Playgroud)

请注意,未指定的行为与未定义的行为略有不同,但它仍然不是理想的行为.

  • @KT:我不认为你对C/C++行为的总结是正确的(不要在C++中重载运算符).C将"x + = y"评估为"x = x + y",其中"x"仅被评估一次(仅在左侧具有副作用时才重要).但是,如果表达式是更大表达式的一部分,例如评估问题的顺序和各种答案和评论中提到的未定义的行为问题,那么一些警告尤其重要.你似乎对这些评论不予理睬,但它们是核心,因为在C中,"x + = x + = 1"是错误的代码,带有无意义的结果. (4认同)

Dre*_*ann 9

在C中,x += x += 1未定义的行为.

您不能指望任何一致的结果,因为尝试在序列点之间两次更新同一对象是未定义的.

  • 我相信你但证明了这一点. (5认同)
  • 为什么会有这么多的赞成?对不起,我没有看到一丝证据!在[Wikipedia](http://en.wikipedia.org/wiki/Operators_in_C_and_C%2B%2B#Operator_precedence)上有一个运算符优先级和关联性列表,看起来`+ =`具有从右到左的关联性.因此可以定义<strike> </ strike>.标准报价? (3认同)
  • @Triptych(C99,6.5p2)"在上一个和下一个序列点之间,一个对象的存储值最多只能通过表达式的计算来修改一次." (3认同)
  • @aaronMcDaid他对C完全正确,不能对Javascript说同样的话. (2认同)
  • @Aaron:这个问题与括号或评估顺序无关.它与发生的副作用的顺序有关,这是有意未指定的.序列点规则用于显式制作任何副作用可能具有依赖于顺序的交互的表达式**未定义的行为**. (2认同)

Dar*_*ust 5

至少在C中,这是未定义的行为.表达式x += x+= 1;有两个序列点:在表达式开始之前的一个隐式序列点(即:前一个序列点),然后再在表达式点处;.在这两个序列点之间x被修改两次,这明确地表示为C99标准的未定义行为.编译器可以自由地做任何它喜欢的事情,包括让守护进程从你的鼻子里飞出来.如果你很幸运,它只是做你期望的,但根本没有保证.

x = x++ + x++;C中未定义的原因相同.另请参阅C-FAQ以获取更多示例和解释或StackOverflow C++ FAQ条目未定义的行为和序列点(AFAIK的C++规则与C相同).


Joh*_*ode 5

这里有几个问题.

首先也是最重要的是C语言规范的这一部分:

6.5表达式
...
2在前一个和下一个序列点之间,一个对象的存储值最多只能通过表达式的计算修改一次.72)此外,先前值应只读以确定要存储的值.73)
...
72)浮点状态标志不是对象,可以在表达式中多次设置.

73)此段落呈现未定义的语句表达式,如
    i = ++i + 1;
    a[i++] = i;
允许的同时
    i = i + 1;
    a[i] = i;

强调我的.

表达式x += 1修改x(副作用).表达式x += x += 1修改x两次没有插入序列点,并且它不是只读取先前值来确定要存储的新值; 因此,行为是未定义的(意味着任何结果都同样正确).现在,为什么地球会成为一个问题呢?毕竟,+=是正确联想的,一切都是从左到右评估的,对吧?

错误.

3语法指示运算符和操作数的分组.74)除如后指定(对于函数调用(),&&,||,?:,和逗号运营商),子表达式的求值的顺序,并且其中的副作用发生均为未指定的顺序.
...
74)语法指定运算符在表达式求值中的优先级,与本子条款的主要子条款的顺序相同,首先是最高优先级.因此,例如,允许作为二元+运算符(6.5.6)的操作数的表达式是6.5.1到6.5.6中定义的表达式.例外情况是强制转换表达式(6.5.4)作为一元运算符的操作数(6.5.3),以及下列任何运算符对之间包含的操作数:分组括号()(6.5.1),下标括号[](6.5.2.1),函数调用括号()(6.5.2.2)和条件运算符?:(6.5.15).

强调我的.

通常,优先级和关联性不会影响评估顺序或应用副作用的顺序.这是一个可能的评估顺序:

 t0 = x + 1 
 t1 = x + t0
 x = t1
 x = t0
Run Code Online (Sandbox Code Playgroud)

哎呀.不是我们想要的.

现在,其他语言,如Java和C#(我假设是Javascript)确实指定操作数总是从左到右进行评估,因此始终有一个明确定义的评估顺序.