c++ - 在 C++11 中, `i +=++i + 1` 是否表现出未定义的行为?

标签 c++ c++11 language-lawyer undefined-behavior

我在阅读(答案)So why is i = ++i + 1 well-defined in C++11? 时提出了这个问题。

我认为微妙的解释是 (1) 表达式 ++i返回一个左值但 +将纯右值作为操作数,因此必须执行从左值到纯右值的转换;这涉及获取该左值的当前值(而不是比 i 的旧值多一),因此必须在增量的副作用之后排序(即更新 i) (2) LHS赋值也是一个左值,所以它的值计算不涉及获取 i 的当前值。 ;虽然此值计算是无序的 w.r.t. RHS 的值计算,这没有问题 (3) 赋值本身的值计算涉及更新 i (再次),但在其 RHS 的值计算之后排序,因此在上次更新到 i 之后;没问题。

很好,所以那里没有 UB。现在我的问题是,如果将赋值运算符从 = 更改为至 += (或类似的运营商)。

Does the evaluation of the expression i += ++i + 1 lead to undefined behavior?



在我看来,标准似乎在这里自相矛盾。自从+=的LHS仍然是左值(并且它的 RHS 仍然是右值),就 (1) 和 (2) 而言,上述推理同样适用;在 += 上的操作数计算中没有未定义的行为.至于(3),复合赋值操作+= (更准确地说是该操作的副作用;如果需要,它的值计算在任何情况下都在其副作用之后排序)现在必须同时获取 i 的当前值,然后(显然是在它之后排序,即使标准没有明确说明,否则对此类运算符的评估将始终调用未定义的行为)添加 RHS 并将结果存储回 i .如果这两个操作是未排序的 w.r.t.,它们都会给出未定义的行为。 ++的副作用,但如上所述(++ 的副作用在 + 的值计算之前排序,给出 += 运算符的 RHS,该值计算在该复合赋值运算之前排序),即不是这样。

但另一方面,标准也说 E += F相当于 E = E + F ,除了(左值)E 只计算一次。现在在我们的例子中计算 i 的值(这就是 E 在这里),因为左值不涉及任何需要对 w.r.t. 进行排序的内容。其他 Action ,所以做一次或两次没有区别;我们的表达式应该严格等同于 E = E + F .但问题来了;很明显,评估 i = i + (++i + 1)会给出未定义的行为!是什么赋予了?或者这是标准的缺陷?

添加。 我稍微修改了我上面的讨论,以更公正地正确区分副作用和值计算之间的区别,并使用表达式的“评估”(作为标准)来包含两者。我认为我的主要问题不仅仅是在这个例子中是否定义了行为,而是一个人必须如何阅读标准才能做出决定。值得注意的是,应该采用 E op= F 的等效项吗?至 E = E op F作为复合赋值运算语义的最终权威(在这种情况下,该示例显然具有 UB),或者仅作为确定要赋值的数学运算所涉及的指示(即由 op 标识的那个) ,将复合赋值运算符的左值到右值转换后的 LHS 作为左操作数,其 RHS 作为右操作数)。正如我试图解释的那样,后一种选择使得在这个例子中为 UB 争论变得更加困难。我承认让等价具有权威性是很诱人的(这样复合赋值成为一种二等原语,其含义是通过重写一等原语来给出的;因此语言定义将被简化),但是有反对这一点是相当有力的论据:
  • 等价不是绝对的,因为“E 只计算一次”异常。请注意,此异常对于避免在 E 的评估中进行任何使用至关重要。涉及副作用未定义行为,例如在相当常见的 a[i++] += b; 中用法。事实上,我认为没有绝对等效的重写来消除复合赋值;使用虚构 |||运算符来指定未排序的评估,可以尝试定义 E op= F; (为简单起见,使用 int 操作数)等同于 { int& L=E ||| int R=F; L = L + R; } ,但是这个例子不再有 UB。在任何情况下,标准都没有给我们重写配方。
  • 该标准不将复合赋值视为不需要单独定义语义的二等原语。例如在 5.17(强调我的)

    The assignment operator (=) and the compound assignment operators all group right-to-left. [...] In all cases, the assignment is sequenced after the value computation of the right and left operands, and before the value computation of the assignment expression. With respect to an indeterminately-sequenced function call, the operation of a compound assignment is a single evaluation.

  • 如果目的是让复合赋值仅仅是简单赋值的简写,则没有理由在此描述中明确包含它们。最后一句甚至直接与如果将等价视为权威的情况相矛盾。

  • 如果承认复合赋值有自己的语义,那么问题就出现了,它们的评估涉及(除了数学运算)不仅仅是副作用(赋值)和值评估(在赋值之后排序),而是还有一个未命名的获取 LHS 的(前一个)值的操作。这通常会在“左值到右值转换”的标题下处理,但在这里这样做很难证明是合理的,因为不存在将 LHS 作为右值操作数的运算符(尽管在扩展中存在一个运算符) “等价”形式)。正是这个未命名的操作与 ++ 的副作用潜在的无序关系。会导致 UB,但是标准中没有明确说明这种未排序的关系,因为未命名的操作不是。使用其存在仅隐含在标准中的操作很难证明 UB 是合理的。

    最佳答案

    关于i = ++i + 1的说明

    I gather that the subtle explanation is that

    (1) the expression ++i returns an lvalue but + takes prvalues as operands, so a conversion from lvalue to prvalue must be performed;



    大概,见 CWG active issue 1642 .

    this involves obtaining the current value of that lvalue (rather than one more than the old value of i) and must therefore be sequenced after the side effect from the increment (i.e., updating i)



    这里的排序是为增量定义的(间接地,通过 += ,参见(a)):++的副作用(对 i 的修改)在整个表达式 ++i 的值计算之前被排序.后者是指计算++i的结果, 不加载 i 的值.

    (2) the LHS of the assignment is also an lvalue, so its value evaluation does not involve fetching the current value of i; while this value computation is unsequenced w.r.t. the value computation of the RHS, this poses no problem



    我认为标准中没有正确定义,但我同意。

    (3) the value computation of the assignment itself involves updating i (again),


    i = expr的值计算仅在您使用结果时才需要,例如int x = (i = expr);(i = expr) = 42; .值计算本身不会修改 i .
    i的修改在表达式 i = expr 中这是因为 =称为=的副作用.这个副作用在 i = expr 的值计算之前被排序-- 或者更确切地说是 i = expr 的值计算在 i = expr 中分配的副作用之后排序.

    通常,表达式的操作数的值计算当然在该表达式的副作用之前进行排序。

    but is sequenced after the value computation of its RHS, and hence after the previous update to i; no problem.



    作业的副作用 i = expr在操作数的值计算之后排序 i (A) 和 expr的任务。
    expr在这种情况下是 + -表达式:expr1 + 1 .此表达式的值计算在其操作数的值计算之后进行排序 expr11 .
    expr1这是++i . ++i的值计算在++i的副作用之后排序(i的修改) (B)

    这就是为什么i = ++i + 1安全 : 在 (A) 中的值计算和 (B) 中对同一变量的副作用之间有一个序列之前的链。

    (a) 标准定义了 ++exprexpr += 1 方面,定义为 expr = expr + 1expr只评估一次。

    为此 expr = expr + 1 ,因此我们只有一个 expr 的值计算. =的副作用在整个expr = expr + 1的值计算之前被排序,并在操作数的值计算后排序 expr (LHS) 和 expr + 1 (右)。

    这符合我对 ++expr 的声明,副作用在++expr的值计算之前被排序.

    关于 i += ++i + 1

    Does the value computation of i += ++i + 1 involve undefined behavior?

    Since the LHS of += is still an lvalue (and its RHS still a prvalue), the same reasoning as above applies as far as (1) and (2) are concerned; as for (3) the value computation of the += operator now must both fetch the current value of i, and then (obviously sequenced after it, even if the standard does not say so explicitly, or otherwise the execution of such operators would always invoke undefined behavior) perform the addition of the RHS and store the result back into i.



    我认为这是问题所在:添加 ii += 的 LHS到 ++i + 1 的结果需要知道 i 的值- 值计算(这可能意味着加载 i 的值)。此值计算相对于 ++i 执行的修改是无序的.这基本上就是您在替代描述中所说的内容,遵循标准 i += expr 强制要求的重写。 -> i = i + expr .这里,i的值计算内i + expr相对于 expr 的值计算而言是未排序的. 这就是您获得 UB 的地方.

    请注意,值计算可以有两个结果:对象的“地址”或对象的值。在表达式中 i = 42 , lhs 的值计算“产生地址” i ;也就是说,编译器需要弄清楚在哪里存储rhs(在抽象机的可观察行为规则下)。在表达式中 i + 42i的值计算产生值(value)。在上面的段落中,我指的是第二种,因此 [intro.execution]p15 适用:

    If a side effect on a scalar object is unsequenced relative to either another side effect on the same scalar object or a value computation using the value of the same scalar object, the behavior is undefined.


    i += ++i + 1 的另一种方法

    the value computation of the += operator now must both fetch the current value of i, and then [...] perform the addition of the RHS



    RHS 是 ++i + 1 .计算此表达式的结果(值计算)相对于 i 的值计算是无序的来自 LHS。所以这句话中的then这个词是有误导性的:当然,它必须先加载i然后将 RHS 的结果添加到其中。但是在 RHS 的副作用和获得 LHS 值的值计算之间没有顺序。例如,您可以获得 LHS 的旧值或新值 i ,由 RHS 修改。

    通常,存储和“并发”加载是数据竞争,这会导致未定义行为。

    处理附录

    using a fictive ||| operator to designate unsequenced evaluations, one might try to define E op= F; (with int operands for simplicity) as equivalent to { int& L=E ||| int R=F; L = L + R; }, but then the example no longer has UB.



    EiF++i (我们不需要 + 1 )。然后,对于 i = ++i
    int* lhs_address;
    int lhs_value;
    int* rhs_address;
    int rhs_value;
    
        (         lhs_address = &i)
    ||| (i = i+1, rhs_address = &i, rhs_value = *rhs_address);
    
    *lhs_address = rhs_value;
    

    另一方面,对于 i += ++i
        (         lhs_address = &i, lhs_value = *lhs_address)
    ||| (i = i+1, rhs_address = &i, rhs_value = *rhs_address);
    
    int total_value = lhs_value + rhs_value;
    *lhs_address = total_value;
    

    这是为了代表我对排序保证的理解。请注意 ,运算符将 LHS 的所有值计算和副作用排序在 RHS 之前。括号不影响排序。在第二种情况下,i += ++i , 我们有 i 的修改未排序的 wrt 左值到右值转换 i => UB。

    The standard does not treat compound assignments as second-class primitives for which no separate definition of semantics is necessary.



    我会说这是一种冗余。来自 E1 op = E2 的重写至 E1 = E1 op E2还包括需要哪些表达式类型和值类别(在 rhs 上,5.17/1 说明了有关 lhs 的一些内容)、指针类型会发生什么、所需的转换等。可悲的是关于“关于 an. .” 在 5.17/1 中并不在 5.17/7 中作为该等价的异常(exception)。

    无论如何,我认为我们应该比较复合赋值与简单赋值加运算符的保证和要求,看看是否有任何矛盾。

    一旦我们在 5.17/7 的异常(exception)列表中加入“关于一个..”,我认为没有矛盾。

    事实证明,正如您在 Marc van Leeuwen 回答的讨论中所见,这句话引出了以下有趣的观察:
    int i; // global
    int& f() { return ++i; }
    int main() {
        i  = i + f(); // (A)
        i +=     f(); // (B)
    }
    

    似乎 (A) 有两种可能的结果,因为对 f 的主体的评估与 i 的值计算不确定地排序在 i + f() .

    另一方面,在(B)中,对 f() 的主体的评估在 i 的值计算之前被排序, 自 +=必须将其视为单个操作,并且 f()当然需要在+=赋值之前进行评估.

    关于c++ - 在 C++11 中, `i +=++i + 1` 是否表现出未定义的行为?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/24194076/

    相关文章:

    c++ - 自增自减运算符重载

    c++ - OpenGL光照

    c++ - 不存储成员函数 c++ 的返回值是否被认为是不好的做法

    c++ - std::rethrow_exception 和抛出的异常类型

    c - 前向声明结构体的范围

    c++ - DLL注入(inject)。带参数执行CreateRemoteThread

    c++ - 通过引用传递、常量引用、右值引用还是常量右值引用?

    c++ - 从 nullptr_t 到 bool : valid or not? 的转换

    c++ - 使用带有参数包扩展和附加值的静态存储持续时间初始化 std::array

    c++ - 使用静态 SFML 库时无法链接我的项目