当前位置: 首页 > 知识库问答 >
问题:

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

贺玉石
2023-03-14

好吧,所以那里没有UB。现在我的问题是,如果将赋值运算符从=更改为+=(或类似运算符),会发生什么。

表达式i+=++i+1的求值是否导致未定义的行为?

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

但另一方面,标准也说E+=fE=E+f等价,只是(lvalue)E只求一次。现在,在我们的示例中,i(这里的e就是这样)的值计算为lvalue,不涉及任何需要排序为w.r.t的内容。其他动作,所以做一两次没什么区别;我们的表达式应该严格等价于E=E+f。但问题是;很明显,计算i=i+(++i+1)会给出未定义的行为!什么给了?还是这是标准的缺陷?

添加。我稍微修改了上面的讨论,以便更好地区分副作用和值计算,并使用表达式的“评估”(和标准一样)来包含这两者。我想我的主要审问不仅仅是关于行为在这个例子中是否被定义,而是一个人必须如何阅读标准,以便决定这一点。值得注意的是,我们应该将eop=fE=eopf的等价性作为复合赋值操作语义的最终权威(在这种情况下,示例明显具有UB),还是仅仅作为确定要赋值的值所涉及的数学操作的指示(即由op标识的,复合赋值运算符的Lvalue到Rvalue转换的LHS作为左操作数,其RHS作为右操作数)。后一个选项使得在本例中为UB辩护变得更加困难,正如我试图解释的那样。我承认,使等价具有权威性是很诱人的(这样复合赋值就变成了一种第二类原语,其意义是通过改写第一类原语来给出的;这样语言定义就简化了),但有相当强烈的论据反对这一点:

>

  • 等价性不是绝对的,因为“e只计算一次”异常。请注意,此异常对于避免在e的计算涉及副作用未定义行为的情况下使用是必不可少的,例如在相当常见的a[i++]+=b;用法中。如果是事实,我认为没有绝对等价的重写来消除复合赋值是可能的;使用虚拟的运算符来指定未排序的计算,可能会尝试将e op=f;(为简单起见,使用int操作数)定义为与{int&L=e int r=f;L=L+r;}等价,但该示例不再具有ub。在任何情况下,标准没有给我们带来任何痛苦的食谱。

    该标准不将复合赋值视为二级原语,对于这些原语不需要单独的语义定义。例如5.17(强调地雷)

    赋值运算符(=)和复合赋值运算符都是从右向左分组的。[...]在所有情况下,赋值都是在右操作数和左操作数的值计算之后、赋值表达式的值计算之前进行排序的。对于不确定顺序的函数调用,复合赋值的操作是单个求值。

    如果我们承认复合赋值有其自身的语义,那么就会出现这样一个问题:它们的求值(除了数学运算之外)不仅仅是一个副作用(赋值)和一个值求值(在赋值之后排序),而且还有一个取LHS的(先前的)值的未命名的操作。这通常在“lvalue-to-rvalue转换”的标题下处理,但在这里这样做很难证明是正确的,因为不存在将LHS作为rvalue操作数的运算符(尽管有一个扩展的“等效”形式)。正是这个未命名的操作,其潜在的未排序关系(带有++的副作用)将导致UB,但这个未排序关系在标准中没有明确说明,因为未命名的操作没有。很难证明UB使用一个操作是合理的,它的存在只是在标准中隐含的。

  • 共有1个答案

    翁翰墨
    2023-03-14

    我想微妙的解释是

    (1)表达式++i返回一个lvalue,但+将prvalues作为操作数,因此必须执行从lvalue到prvalue的转换;

    可能,请参阅CWG活动问题1642。

    (2)赋值的LHS也是一个L值,因此其值求值不涉及取i的当前值;而这个值的计算是没有顺序的W.R.T.RHS的值计算,这没有问题

    我不认为这是正确的定义在标准,但我同意。

    (3)赋值本身的值计算涉及更新i(再次),

    在表达式i=expr中,由于=而发生的对i的修改称为=的副作用。此副作用在i=expr的值计算之前排序--或者更确切地说,i=expr的值计算在i=expr中赋值的副作用之后排序。

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

    而是在其RHS的值计算之后,因此在前一次更新到i之后进行排序;没问题.

    本例中的expr+表达式:expr1+1。此表达式的值计算在其操作数expr11的值计算之后排序。

    这里的expr1++I++i的值计算顺序是在++i的副作用(i的修改)(B)之后

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

    (a)标准以expr+=1定义++expr,定义为expr=expr+1expr只计算一次。

    因此,对于这个expr=expr+1,我们只有一个expr的值计算。=的副作用在整个expr=expr+1的值计算之前排序,在操作数expr(LHS)和expr+1(RHS)的值计算之后排序。

    这与我的主张相对应,即对于++expr,副作用在++expr的值计算之前进行排序。

    i+=++i+1的值计算是否涉及未定义的行为?

    由于+=的LHS仍然是一个L值(其RHS仍然是一个prvalue),就(1)和(2)而言,同样的推理也适用;对于(3)+=运算符的值计算现在必须同时获取i的当前值,然后(显然是在它之后排序的,即使标准没有明确说明,否则执行这样的运算符总是调用未定义的行为)执行RHS的添加并将结果存储回i中。

    我认为问题是这样的:在I+=的LHS中将I添加到++I+1的结果中,需要知道I的值--一个值计算(这可能意味着加载I的值)。对于++i执行的修改,该值计算是不排序的。在标准i+=expr->i=i+expr要求的重写之后,这基本上是您在备选描述中所说的。这里,i+expr中的i的值计算相对于expr的值计算是不排序的。那就是你得到UB的地方。

    请注意,值计算可以有两个结果:对象的“地址”,或者对象的值。在表达式i=42中,lhs的值计算“产生i的地址”;也就是说,编译器需要找出将rhs存储在何处(根据抽象机器的可观察行为规则)。在表达式i+42中,i的值计算产生值。在上一段中,我指的是第二种,因此[intr.execution]P15适用:

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

    +=运算符的值计算现在必须同时获取i的当前值,然后[...]执行RHS的添加

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

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

    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;
    
        (         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的Lvalue到Rvalue的转换。

    该标准不将复合赋值视为二级原语,对于这些原语不需要单独的语义定义。

    我会说那是多余的。从E1 op=e2E1=E1 ope2的重写还包括需要哪些表达式类型和值类别(在rhs上,5.17/1说明了lhs的一些内容)、指针类型发生了什么、所需的转换等等。可悲的是,关于“with respect to an...”的句子在5.17/1中不是在5.17/7中作为该等价的例外。

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

    一旦我们把它说成“相对于一个...”同样在5.17/7中的例外列表中,我不认为有矛盾。

    事实证明,正如你在Marc van Leeuwen答案的讨论中所看到的,这句话引出了以下有趣的观察:

    int i; // global
    int& f() { return ++i; }
    int main() {
        i  = i + f(); // (A)
        i +=     f(); // (B)
    }
    

    似乎(A)有两种可能的结果,因为f体的求值与i+f()中的i的值计算是不确定的顺序。

     类似资料:
    • 如果标量对象上的副作用相对于同一标量对象上的另一个副作用或使用同一标量对象的值进行的值计算没有排序,则行为是未定义的。 缺陷报告中的解释没有帮助我理解。lvalue-to-rvalue转换与任何事情有什么关系?我错在哪里了?

    • 我一直在温习我未定义的行为规则,并阅读了以下内容: 未定义的行为和序列点 为什么f(i=-1,i=-1)行为未定义 为什么`x-- 在C 11中,“i = i 1”是否表现出未定义的行为? 最后有三个问题: < li >形式为< code>i=i 的术语的未定义行为规则是否适用于非整型?(表达式应翻译为< code > I . operator(I . operator(I)),由于每个函数调用都

    • 问题内容: 哪个更正确?Java的结果为12或C =13。或者,如果不是正确性,请详细说明。 问题答案: 没有比这更正确的了。它实际上是未定义的,称为序列点错误。 http://en.wikipedia.org/wiki/Sequence_point

    • 考虑以下C程序: null 访问易失性对象、修改对象、修改文件,或者调用执行那些操作中的任何操作的函数都是副作用,它们是执行环境状态的改变。表达式的计算通常包括值计算和副作用的启动。用于lvalue表达式的值计算包括确定指定对象的标识。 Sequenced before是单线程执行的计算之间的非对称、传递、成对关系,它导致这些计算之间的部分顺序。给定任意两个评价A和B,如果A排序在B之前,那么A的

    • 问题是,如果变量i是int的话,哪一个性能更好。 我正在寻找性能方面的内存块或寄存器所需的和CPU周期所需的这两种情况。

    • 我在英国大学考试中遇到了这个难题。 考虑以下循环,到目前为止,其中i未声明: 找到的定义,它在这个循环之前,这样time循环就会永远继续。 下一个问题,对该代码段提出了相同的问题: 对我来说是显而易见的。当然,在另一种情况下,它是NaN,但我真的被前一种情况困住了。这与溢出有关吗?是什么导致这样的循环在Java中永远循环?