++ x%= 10在C++中定义良好吗?

kyk*_*yku 56 c++ evaluation undefined-behavior language-lawyer

在浏览某个项目的代码时,我遇到了以下声明:

++x %= 10;
Run Code Online (Sandbox Code Playgroud)

这个陈述是用C++很好地定义的,还是属于同一类别

a[i] = i++
Run Code Online (Sandbox Code Playgroud)

pax*_*blo 63

根据C++ 11 1.9 Program execution /15:

除非另有说明,否则对单个运算符的操作数和单个表达式的子表达式的评估是不确定的.

如果对标量对象的副作用相对于同一标量对象的另一个副作用或使用相同标量对象的值进行的值计算未被排序,则行为未定义.

在这种情况下,我认为++x是一个副作用,x %= 10是一个值计算,所以你认为它将是未定义的行为.但是,赋值部分(5.17 /1)可以这样说(我的粗体):

在所有情况下,在右和左操作数的值计算之后,以及在赋值表达式的值计算之前,对赋值进行排序.

因此,这意味着双方在分配之前和分配结果可用之前进行排序.并且由于标准也声明(5.17 /7)只与被评估一次x OP = y相同,x = x OP yx事实证明这明确定义的行为,因为它相当于:

++x = Q % 10; // where Q is the value from ++x, not evaluated again.
Run Code Online (Sandbox Code Playgroud)

然后唯一的问题是分配的哪一方被评估,因为它们没有排序.但是,在这种情况下我认为不重要,因为这两者都会产生同样的效果:

++x = Q % 10; // LHS evaluated first
Q = ++x % 10; // RHS evaluated first
Run Code Online (Sandbox Code Playgroud)

现在,这是对标准的解读.虽然我在解码复杂文档方面有相当多的经验,但我可能会错过一些东西 - 我不这么认为,因为我们在这里得到了热烈的讨论:-)而我认为我们所有人都建立了相关部门.

但是,无论是否定义好的,体面的编码员都不应该编写这样的代码.自从PDP minis的低内存/小存储天以来已经很长时间了,现在是时候我们编写代码才能读取.

如果你想增加然后取模,使用x = (x + 1) % 10,如果只是为了让下一个可怜的Joe读取该代码更容易理解.

  • 实际上,你引用一个好的.但到目前为止还不够,如在`++ x`中,`x`的修改在表达式的值之前排序. (2认同)
  • 看看一元++和 - 的定义.测序绝对没有问题,一切都很明确. (2认同)
  • 但是有可能延迟副作用.`i = ++ i + 1;`在C++ 03的第5节开头特别提到未定义.编译器可以想象生成`++ x%= 10`的代码,它将`x`带入寄存器,递增它,将结果移位到另一个寄存器,进行除法,将其结果移动到`x的存储位置`然后将先前计算的增量结果移动到相同的存储器地址.该操作未在C++ 03抽象机中定义,并且接近金属存在很大的惊喜空间. (2认同)

Col*_*mbo 22

TL; DR:定义明确,因为x保证在赋值之前递增.


一些C++ 11标准

[intro.execution]/15:

除非另有说明,否则对单个运算符的操作数和单个表达式的子表达式的评估是不确定的.

但是,[expr.ass]/1确实注意到:

在所有情况下,在右和左操作数的值计算之后,以及在赋值表达式的值计算之前,对赋值进行排序.

所以这确实构成了第一个引用的例外.此外,如[expr.pre.incr] 1中所述,++x等同于x += 1上述引用所涵盖的内容:赋值在值计算之前被排序.因此,++x在值计算之前对增量进行排序.

考虑到这一点,不难看出,++x %= 10增量是在分配之前完成的.
因此,在赋值副作用之前对增量副作用进行测序,因此对所有涉及的副作用进行测序.

换句话说,标准规定了以下顺序:

  • ++x并且10被评估 - 其顺序10未被排序,但仅仅是文字,因此在这里不相关.
    • ++x 被评估:
      • 首先,x增加值.
      • 然后完成值计算,我们得到一个左值x.
  • 任务完成.更新后的值x采用模数10并分配给x.
  • 可以遵循赋值的值计算,在赋值后清楚地对其进行排序.

于是

如果对标量对象的副作用相对于同一标量对象的另一个副作用或使用相同标量对象的值进行的值计算未被排序,则行为未定义.

不适用,因为副作用和价值计算是有序的.


1不是[expr.post.incr],它将用于后缀增量!


Ded*_*tor 20

让我们看一元增量运算符:

5.3.2递增和递减[expr.pre.incr]

1前缀的操作数++通过添加1来修改,或者设置为trueif bool(如果不推荐使用).操作数应是可修改的左值.操作数的类型应为算术类型或指向完全定义的对象类型的指针.结果是更新的操作数; 它是一个左值,如果操作数是一个位域,它是一个位域.如果x不是类型bool,则表达式++x等效于x+=1.
[...]

因此,与该一元运算符有关的所有评估和副作用都在其值之前进行调度,因此不会造成严重破坏.

剩下的就是评估%= 10左值.只评估常量可能是并发的(不可能造成任何伤害),其余部分在其他所有内容之后严格排序.


OJF*_*ord 5

我将提供一个替代答案,而不引用好书,因为我认为重写它会使其显而易见.

++x %= 10;       // as stated
x += 1 %= 10;    // re-write the 'sugared' ++x
Run Code Online (Sandbox Code Playgroud)

这让我觉得很清楚.我们知道,赋值的结果(如果我们真的想要,仍然'加糖' +=减少到)本身就是一个左值,所以毫无疑问,通过进一步减少表达式是:

(x = x+1) %= 10  // += -> =1+
x = (x+1) % 10   // assignment returns reference to incremented x
Run Code Online (Sandbox Code Playgroud)

  • 不!这是正确的扩展:`x =(x = x + 1)%10` 2序列点之间的分配 - >未定义 (3认同)

hac*_*cks 5

在表达中

++x %= 10;  
Run Code Online (Sandbox Code Playgroud)

最令人困惑的部分是x在两个序列点之间修改两次,一次是前缀++,一次是结果分配.这让人觉得上面的表达式会引用未定义的行为,就像我们在旧的C++中学到的那样

在前一个和下一个序列点之间,标量对象应通过表达式的计算最多修改其存储值一次.

在C++ 11中,规则是不同的(它是关于排序而不是序列点!):

如果对标量对象的副作用相对于同一标量对象的另一个副作用或使用相同标量对象的值进行的值计算未被排序,则行为未定义.

正如我们已经知道的那样++x,上面的表达式只会被评估一次(并且会给出一个左值),因为

C++ 11:5.17

表单的表达式的行为E1 op = E2等同于E1 = E1 op E2 E1计算一次的表达式.

也知道操作数的评估++x10将发生的结果计算之前,%=操作员通过标准:

在运算符的结果的值计算之前,对运算符的操作数的值计算进行排序.

结论:

++x只有在给出左值并且仅在%=执行该操作之后才会被评估.这意味着,两个修改都x被排序,并且上面的表达被很好地定义.