++x %= 10 在 C++ 中是不是明确定义?

Posted

技术标签:

【中文标题】++x %= 10 在 C++ 中是不是明确定义?【英文标题】:Is ++x %= 10 well-defined in C++?++x %= 10 在 C++ 中是否明确定义? 【发布时间】:2015-03-06 07:30:29 【问题描述】:

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

++x %= 10;

这个语句在 C++ 中定义得很好还是属于同一类

a[i] = i++

?

【问题讨论】:

那么,@Dedup,您要引用相关参考资料,是吗?因为,据我所知,对于 C++11,它 仍然 未定义。如果我错了,我不介意被纠正,但只是在某处参考实际标准:-) 关闭项目,用火烧掉! 你可以投票决定它应该做什么:herbsutter.com/2014/12/01/… 我很抱歉你不得不处理这段代码。原作者还在某处工作吗?我想避免与他/她相关的任何产品。这种“语法”是在乞求错误。甚至不要问它是否合法。将其更改为可读代码 - 使意图明确。 @Floris:我猜 OP 会在他知道代码的实际作用后立即执行此操作。此外,一些雇主不会让你修复这样的事情,因为他们害怕在已经摇摇欲坠的系统中引入错误。 【参考方案1】:

按照 C++11 1.9 Program execution /15:

除非另有说明,否则单个运算符的操作数和单个表达式的子表达式的求值是无序的。

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

在这种情况下,我相信++x 是一个副作用,x %= 10 是一个值计算,所以你会认为它是未定义的行为。但是,assignment 部分 (5.17 /1) 有这样的说法(我的粗体字):

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

因此,这意味着双方在分配之前和分配结果可用之前都已排序。而且由于标准还规定 (5.17 /7) x OP = yx = x OP y 相同,但 x 只被评估一次,结果证明这个定义明确的行为,因为它是等价的到:

++x = Q % 10; // where Q is the value from ++x, not evaluated again.

剩下的唯一问题是赋值的哪一个被评估,因为它们没有被排序。但是,在这种情况下,我认为这并不重要,因为这两种情况都会产生相同的效果:

++x = Q % 10; // LHS evaluated first
Q = ++x % 10; // RHS evaluated first

现在,这就是我的对标准的解读。虽然我在解码复杂文档方面拥有丰富的经验,但可能有些我错过了 - 我不这么认为,因为我们都在这里进行了热烈的讨论以达到这一点:-) 我想我们都已经建立了相关的部分。

但是,无论是否定义良好,体面的程序员不应该编写这样的代码。距离 PDP mini 的低内存/小存储时代已经过去了很长时间,现在是时候让我们的代码可读了。

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

【讨论】:

其实你引用的不错。但到目前为止还不够,因为在++x 中,x 的修改在表达式的值之前排序。 @Dedup,您的参考在哪里? 1.9 指出,除非另有说明,否则它是未排序的。我在5.3.2 Unary incr/decr 中看不到任何指定排序的内容。从 5.3 开始搜索,单词 sequenced 直到 after unary incr/decr. 的两个部分才会出现 看一下一元++和--的定义。排序绝对没有问题,一切都定义明确。 @paxdiablo:但问题在于,从 5.3.2 开始:If x is not of type bool, the expression ++x is equivalent to x+=1。这就是在这个子表达式中的排序变得明确的地方(x = x + 1)。当然,对于 OP 问题中的示例,由于完整表达式的其余部分中的不确定顺序,这是一个有争议的问题。您可以在完整表达式中以某种方式组合定义明确的子表达式,如果子表达式的不确定顺序在整体上呈现完整的完整表达式-表达式 UDB。 但是有延迟副作用的可能性。 i = ++i + 1; 在 C++03 的第 5 节开头特别提到为未定义。可以想象,编译器可以为++x %= 10 生成代码,将x 放入寄存器,将其递增,将结果转移到另一个寄存器,进行除法,将结果移动到x 的内存位置,然后移动先前计算的递增到同一内存地址的结果。操作在C++03抽象机中没有定义,接近金属有很大的惊喜空间。【参考方案2】:

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


一些 C++11 标准

[intro.execution]/15:

除非另有说明,单个运算符的操作数的评估 和单个表达式的子表达式是无序的。

但是,[expr.ass]/1 确实注意到了一些事情:

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

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

考虑到这一点,不难看出在++x %= 10 中,增量是在赋值之前完成的。 所以增量副作用在赋值副作用之前排序,因此所有涉及的副作用都是排序的。

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

++x10 被评估 - 其顺序是无序的,但 10 只是一个文字,所以在这里不相关。 ++x 被评估: 首先,x 的值递增。 然后值计算完成,我们得到一个引用x的左值。 任务完成。 x 的更新值取模 10 并分配给 x。 赋值的值计算可能紧随其后,在赋值之后有明确的顺序。

因此

如果标量对象的副作用相对于其中任一对象而言是未排序的 对同一标量对象或值计算的另一个副作用 使用相同标量对象的值,行为未定义。

不适用,因为副作用和值计算是按顺序排列的


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

【讨论】:

【参考方案3】:

我们来看看一元自增运算符:

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

1 前缀++的操作数加1修改,如果是bool则设置为true(此用法已弃用)。操作数应为可修改的左值。操作数的类型应为算术类型或指向完全定义的对象类型的指针。 结果是更新后的操作数;它是一个左值,如果操作数是一个位域,它就是一个位域。如果x 不是bool 类型,则表达式++x 等价于x+=1。 [...]

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

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

【讨论】:

当然不是。因为没有说“严格之后”,因为在整个表达式中有 2 个赋值,唯一真正的序列点是周围的 ; @v.oddou:你忘记了顺序关系。在将操作数本身作为左值返回之前,一元增量会完成所有工作(增加操作数)。 是的,你是对的。我想我现在明白了,就像我在对 hacks 答案的评论中提到的那样。然而这仅在 C++11 中是正确的,在这种情况下 C++03 是 UB。我的第一个 cmets 在 03 年申请。【参考方案4】:

在表达式中

++x %= 10;  

最令人困惑的部分是x 在两个序列点之间修改了两次,一次通过前缀++,一次通过结果赋值。这给人的印象是,上面的表达式调用了未定义的行为,就像我们在旧 C++ 中学到的那样

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

在 C++11 中,规则不同(它是关于 sequencing 而不是 sequence points!):

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

我们已经知道++x 是上面的表达式只会被计算一次(并且会给出一个左值),因为

C++11:5.17

E1 op = E2 形式的表达式的行为等同于 E1 = E1 op E2除了 E1 只计算一次

并且还知道操作数 ++x10 的评估将按照标准在计算 %= 运算符的结果之前进行:

运算符的操作数的值计算在运算符结果的值计算之前排序。

结论:

++x 只会被评估一次,给出一个左值,并且只有在那之后才会执行%= 操作。 这意味着,对x 的两次修改都是有序的,并且上面的表达式定义良好。

【讨论】:

你走的捷径太多了。值是但不是副作用。因此,由于左侧的副作用与%= 的副作用冲突,仍然未排序且未定义,标准中没有对它们进行排序。但是 +1 提到了一些看起来像 C++03 和 11 的强大差异的东西! @v.oddou; ++x 然后%= 被排序。 是的,在 Joannes Shlaub 的回答中阅读了更多关于此的内容后,我现在同意仅在 C++11 中,确实 prefix ++x 是按其副作用排序的,因为该值必须按标准排序并且运算符返回lvalue。这一点都不明显,但现在我明白了。【参考方案5】:

我将提供一个替代答案,而不是引用这本好书,因为我认为稍微重写一下就很明显了。

++x %= 10;       // as stated
x += 1 %= 10;    // re-write the 'sugared' ++x

这在我眼中已经足够清晰了。正如我们所知,赋值的结果(如果我们真的想要的话,仍然“加糖”的+= 归约为)本身就是一个左值,所以毫无疑问,通过进一步归约,表达式是:

(x = x+1) %= 10  // += -> =1+
x = (x+1) % 10   // assignment returns reference to incremented x

【讨论】:

不!这是正确的扩展:x = (x=x+1) % 10 序列点之间的 2 个分配 -> 未定义 @v.oddou,C++11 中没有序列points,它只是处理各种动作的序列。而且,根据标准,赋值的两侧在赋值本身发生之前完全排序。因此,您的语句中的第二个= 在第一个之前完成。 -> 已定义。 @paxdiablo 我认为你是对的。但正如我发现的那样,这仅适用于 C++11。在 C++03 中,它仍然像我说的那样发生。【参考方案6】:

嗯,它是定义的。作为未定义的行为:

§1.9/15: 如果标量对象的副作用相对于同一标量对象的另一个副作用或使用同一标量对象的值的值计算是未排序的,则行为未定义。

这里有两个未排序的副作用。

鉴于表达式++x %= 10;从左到右移动,我们有:

    值计算(x+1) 修改对象=,如x = x + 1),例如副作用每 §1.9/12 一个不确定序列操作(%=),它本身同时具有值​​计算('%')和对象修改副作用(' =')(同上,1,2)在同一个标量对象('x')上。

完整表达式中的两个子表达式是无序的相对于彼此。虽然我们从从左到右开始阅读,但它们是不确定顺序的,因此 §1.9/13 中明确没有偏序救助:

给定任意两个评估AB,如果AB之前排序,则执行A 应先于 B 的执行。如果 A 没有排在 B 之前并且 B 没有排在 A 之前,那么 A em> 和 B未排序的

所以,UDB。

【讨论】:

当我看到你的第二句话时,我正伸手去按下投票按钮。轻笑:-) 是的,坦率地说,我会放弃第一个。请记住,很多人的英语水平很低。人们不阅读,您的第一句话本身就是完全错误的。 @T.J.Crowder:点了。编辑第一句/第二句以适应 ESL。 看一下一元++和--的定义。排序绝对没有问题,一切都定义明确。 @Deduplicator:我认为这将在另一个评论讨论中继续进行,所以我会在那里关注它,但请考虑 5.3.2 中的等效性与 OP 示例中%= 中的问题 - - 您的评论根本没有解决完整表达式上下文中%= 左侧的究竟是什么 的问题。我不一定说你错了 - 我是说你的评论没有参考基本上等于说“没什么可看的,伙计们,继续前进”【参考方案7】:

前缀增量 (++x) 的模数分配 (%= ) 具有最高优先级。语句:++x %= 10;可以表示为:

++x;
x%= 10;

【讨论】:

以上是关于++x %= 10 在 C++ 中是不是明确定义?的主要内容,如果未能解决你的问题,请参考以下文章

报表生成器:明确的 X 轴范围定义?

0.0 / 0.0是一个定义明确的值吗?

C++中,怎么输出一个n阶矩阵呢?

C和C++是不是都可以函数嵌套调用,但是不能函数嵌套定义?

C++,2D std::vector,我是不是需要明确保留和推回空向量?

为啥 C 或 C++ 标准不明确将 char 定义为有符号或无符号?