在C++中,++x %= 10是否有明确定义?

58

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

++x %= 10;

这个语句在C++中是否被定义良好,还是与之前提到的同类问题?

a[i] = i++

?


3
所以,@Dedup,你将会“引用”相关的参考文献,是吗?因为就我所知,对于C++11来说,它仍然是未定义的。如果我错了,我不介意被纠正,但必须提供对实际标准的参考文献。 :-) - paxdiablo
24
关闭这个项目,然后用火把它烧掉! - user1178540
3
你可以投票决定它应该做什么:http://herbsutter.com/2014/12/01/a-quick-poll-about-order-of-evaluation/ - Thomas W.
11
我很同情你需要处理这段代码。原始作者还在某个公司任职吗?我想避免与他/她有关的任何产品。这种“语法”非常容易出错。甚至不要问它是否合法。将其更改为易读的代码,使意图明确而不含歧义。 - Floris
2
@Floris:我猜OP会在知道代码实际作用后立即这样做。此外,一些雇主不会让你修复这样的问题,因为他们担心会在已经不稳定的系统中引入错误。 - Kevin
显示剩余21条评论
7个回答

64

根据C++11标准中的1.9程序执行/15

除非另有说明,否则各个运算符的操作数和各个表达式的子表达式的求值顺序是未指定的。

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

在这种情况下,我认为++x是副作用,而x %= 10是值计算,所以你可能认为它是未定义行为。然而,赋值部分(5.17 /1)有如下说明(我的加粗):

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

因此,这意味着在赋值之前,两边的计算顺序已经确定,在赋值的结果可用之前也已经确定。由于标准还规定(5.17 /7x 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小型机的低内存/小存储时代以来已经过去了很长时间,是时候将我们的代码编写成易于阅读的形式了。

如果您想要增量再取模,使用x = (x + 1) % 10,即使只是为了让下一个可怜的Joe更容易理解那段代码。


2
实际上,你引用了一个好的例子。但是远远不够,因为在++x中,对x的修改在表达式的值之前被排序。 - Deduplicator
1
@Dedup,你的参考资料在哪里?1.9规定,除非另有说明,否则是无序的。在5.3.2一元自增/自减中,我看不到任何指定排序的内容。从5.3开始搜索,直到一元自增/自减两个部分之后才出现排序这个词。 - paxdiablo
2
看一下单目运算符++和--的定义。在顺序方面没有任何问题,一切都是明确定义的。 - Deduplicator
1
对于 ++lvalue: "结果是更新后的操作数;它是一个左值。" 你还想要更详细明确的吗? - Deduplicator
2
但是副作用被延迟的可能性存在。在C++03的第5节开头,明确提到i = ++i + 1;是未定义的。编译器可以构建代码来处理++x %= 10,将x存入寄存器中,对其进行递增操作,将结果移动到另一个寄存器中进行移位运算,再进行除法运算,将结果移动到x的内存位置,并将之前计算出的递增结果移动到同一内存地址。该操作在C++03抽象机器中未定义,在接近底层的情况下,会有很多意外情况发生。 - Wintermute
显示剩余25条评论

22

简而言之:由于在赋值之前,保证了 x 的递增,因此这是一个定义明确的过程。


C++11标准的一些术语

[intro.execution]/15:

除非有特别说明,否则单个操作符的操作数和单个表达式的子表达式的计算顺序是未定义的。

然而,[expr.ass]/1有所说明:

在所有情况下,赋值都在右操作数和左操作数的值计算之后进行,并在赋值表达式的值计算之前进行。

因此,这构成了第一个引用中的例外。此外,如[expr.pre.incr]1所述,++x等同于x += 1,它也被上述引用所覆盖:赋值在值计算之前被排序。因此对于++x,递增在值计算之前被排序。

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

换句话说,标准强制执行以下排序:

  • 评估++x10,它们的顺序未定义,但10只是一个文字,所以这里不相关。
    • 评估++x
      • 首先,x的值递增。
      • 然后进行值计算,并得到引用x的lvalue。
  • 完成赋值。以x更新的值对10取模,并将其分配给x
  • 在赋值之后,可能会出现其值计算的情况,这显然是在赋值之后顺序执行的。
  • 因此,如果对标量对象的副作用与同一标量对象的另一个副作用或使用相同标量对象值的值计算不是按顺序进行的,则行为是未定义的。

    由于副作用和值计算是有序的,因此上述规则不适用。


    1并非[expr.post.incr],那将是后缀递增运算符!


    20

    让我们来看一下单目递增运算符:

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

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

    因此,所有与该单目运算符相关的计算和副作用都在其值之前安排好了,因此不会造成混乱。

    现在只需对该左值进行%= 10的评估即可。只有对常量进行评估可能是并发的(这不可能造成任何伤害),其余部分严格按照其他所有内容之后的顺序进行。


    当然不是。因为在整个表达式中有两个赋值操作,唯一真正的序列点是周围的;,没有任何东西能够明确地表示“严格之后”。 - v.oddou
    1
    @v.oddou:你忘记了顺序关系。而一元递增在返回操作数本身作为左值之前,会先完成所有的工作(即递增操作)。 - Deduplicator
    是的,你说得对。我现在明白了,就像我在haccks的回答中提到的评论一样。然而,这只适用于C++11,在C++03中则是未定义行为。我的第一个评论适用于03。 - v.oddou

    6
    在表达式中
    ++x %= 10;  
    

    最令人困惑的是,在两个序列点之间,变量 x 被前缀 ++ 和赋值操作修改了两次。这给人一种印象,即上述表达式会引发未定义行为,因为我们曾经在旧版 C++ 中学到过:
    “在前一个和后一个序列点之间,标量对象通过求值最多只能被修改一次。”
    在 C++11 中,规则有所不同(它与“序列点”无关,而是涉及“顺序”!):
    “如果对标量对象的副作用相对于同一标量对象的另一个副作用或使用该标量对象的值进行的值计算是未排序的,则行为未定义。”
    由于我们已经知道 ++x 在上述表达式中仅被评估一次(并将产生一个左值),因此:
    C++11:5.17
    “形如 E1 op = E2 的表达式的行为等同于 E1 = E1 op E2,除了 E1 仅被评估一次。”
    我们还知道,在计算 %= 运算符的结果之前,操作数 ++x 和 10 的评估将发生,因为按照标准:
    “运算符的操作数的值计算在运算符的结果的值计算之前排序。”
    结论:
    ++x 只会被评估一次,得到一个左值,然后才会执行 %= 操作。 这意味着,对 x 的两次修改是有序的,上述表达式是定义良好的。

    你正在采取太多的捷径。值是对的,但副作用不是。因此,仍然是未排序的,并且仍然未定义,因为左侧的副作用与“%=”的副作用冲突。在标准中没有任何序列。但是+1提到了一个看起来像是C++03和11之间的重大区别! - v.oddou
    @v.oddou; ++x 然后 %= 是有序的。 - haccks
    是的,在阅读了Joannes Shlaub的回答后,我现在同意仅在C++11中,确实前缀++x在其副作用上被排序,因为该值必须按标准排序,并且运算符返回一个lvalue。这一点并不明显,但现在我看到了。 - v.oddou

    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
    

    3
    不!这是正确的展开方式:x = (x=x+1) % 10。在序列点之间进行了两次赋值 -> 未定义。 - v.oddou
    @v.oddou,C++11中没有序列点,它只处理各种操作的排序。根据标准,在赋值发生之前,赋值语句的两侧被完全排序。因此,在你的语句中第二个=在第一个之前完成。->已定义。 - paxdiablo
    @paxdiablo 我认为你是对的。但是我发现,这只适用于C++11。在C++03中,仍然会发生我所说的情况。 - v.oddou

    4
    好的,它是已经定义。作为未定义行为:
    §1.9/15:如果标量对象上的副作用与同一标量对象上的另一个副作用或使用同一标量对象的值计算相对不确定,那么行为是未定义的。
    这里有两个未确定的副作用。
    给定表达式 ++x %= 10; 从左到右移动,我们有:
    1. 值计算 (x+1) 2. 修改对象 (=, 如 x = x + 1), 例如根据 §1.9/12 每次执行都会产生一个副作用 3. 不确定序列的操作 (%=) 本身具有值计算 ('%') 和对象修改的副作用 ('=') (来自 1、2) 在同一标量对象 ('x') 上。
    完整表达式中的两个子表达式相对不确定。虽然我们开始从左到右读取它们,但这些是不确定的,在 §1.9/13 中明确禁止了部分排序:
    给定任意两个评估 A 和 B,如果 A 在 B 之前排序,则执行 A 应在执行 B 之前。如果 A 不会排在 B 之前,而 B 也不会排在 A 之前,则 A 和 B 是不确定的。
    所以,行为未定义。

    5
    当我看到你的第二句话时,我正想按下踩(dislike)按钮。哈哈。 - paxdiablo
    1
    是的,坦白说我会放弃第一个。记住很多人英语水平很低。而且人们不喜欢阅读,你的第一句话本身就是错误的。 - T.J. Crowder
    @T.J.Crowder:收到。编辑第一/第二句话以适应ESL。 - frasnian
    1
    看一下单目运算符++和--的定义。在顺序方面没有任何问题,一切都是明确定义的。 - Deduplicator
    1
    @Deduplicator:我认为这个问题在另一个评论讨论中继续,所以我会在那里跟进,但请考虑5.3.2中的等效性与OP示例情况中%=的问题——您的评论根本没有解决完整表达式上左侧是什么的问题。我并不是说您错了——我是说,如果没有参考资料,您的评论基本上相当于说“这里没有什么可看的,大家都散了吧”。 - frasnian
    显示剩余7条评论

    1

    前缀递增 (++x) 的优先级高于取模赋值 (%=)。语句:++x %= 10; 可以表示为:

    ++x;
    x%= 10;
    

    网页内容由stack overflow 提供, 点击上面的
    可以查看英文原文,
    原文链接