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

时间:2014-06-12 21:17:29

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

我正在阅读({3}}

的答案时出现了这个问题

我认为,微妙的解释是:(1)表达式++i返回左值,但+将prvalues作为操作数,因此必须执行从左值到右值的转换;这涉及获取该左值的当前值(而不是i的旧值),因此必须在增量的副作用后对其进行排序(即更新{{ 1}})(2)赋值的LHS也是左值,因此其值评估不涉及获取i的当前值;而这个值的计算是未经测试的w.r.t. RHS的值计算,这没有问题(3)赋值本身的值计算涉及更新i(再次),但是在其RHS的值计算之后排序,因此在更新之后i;没问题。

很好,所以那里没有UB。现在我的问题是,如果将分配运算符从i更改为=(或类似的运算符),该怎么办。

  

表达式+=的评估是否会导致未定义的行为?

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

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

已添加。我稍微修改了上面的讨论,更加公平地区分副作用和价值计算,并使用"评估" (如同标准一样)表达式包含两者。我认为我的主要询问不只是在这个例子中是否定义了行为,而是如何必须阅读标准才能做出决定。值得注意的是,是否应该将i = i + (++i + 1)E op= F的等价作为复合赋值操作语义的最终权限(在这种情况下,该示例显然具有UB),或仅仅作为数学的指示操作涉及确定要分配的值(即由E = E op F标识的值,复合赋值运算符的左值到右值转换LHS为左操作数,其RHS为右操作数)。正如我试图解释的那样,后一种选择使得在这个例子中为UB争论更加困难。我承认,使等价具有权威性是很诱人的(因此复合赋值成为一种二级原语,其含义是通过用一等原语重写来给出的;因此语言定义会被简化),但那里反对这是相当强烈的论据:

  • 等价不是绝对的,因为" op仅被评估一次"例外。请注意,此异常对于避免在E的评估涉及副作用未定义行为时进行任何使用至关重要,例如在相当常见的E用法中。事实上,我认为没有绝对等同的重写来消除复合分配是可能的;使用虚构的a[i++] += b;运算符来指定未经测序的评估,可以尝试将|||(简化为E op= F;个操作数)定义为等同于int,但随后不再使用示例有UB。在任何情况下,标准都没有给我们重新配方。

  • 该标准不会将复合赋值视为第二类原语,因此不需要单独的语义定义。例如在5.17(强调我的)

      

    赋值运算符(=)和复合赋值运算符从右到左分组。 [...] 在所有情况下,分配在值之后排序   计算右和左操作数,并在赋值表达式的值计算之前。对于不确定顺序的函数调用,复合赋值的操作是单一评估

  • 如果意图让复合作业仅仅是简单作业的缩写,那么就没有理由在这个描述中明确地包括它们。最后一句话甚至直接与如果等同性被认为具有权威性的情况相矛盾。

如果一个人承认复合作业具有自己的语义,那么就会产生这样的观点:他们的评价不仅仅涉及副作用(作业)和价值评估(在作业之后排序)。 ),但也是一个获取LHS(先前)值的未命名操作。这通常在&#34;左值到右值转换&#34;的标题下处理,但这样做很难证明,因为没有运算符存在将LHS 作为右值< / em>操作数(虽然在扩展的&#34;等效的&#34;形式中有一个)。恰恰是这个未命名的操作,其与{ int& L=E ||| int R=F; L = L + R; }的副作用的潜在的无序关系将导致UB,但是这种未经测序的关系在标准中没有明确说明,因为未命名的操作不是。使用一种只存在于标准中的操作很难证明UB的合理性。

4 个答案:

答案 0 :(得分:16)

关于i = ++i + 1

的说明
  

我认为微妙的解释是

     

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

或许,请参阅CWG active issue 1642

  

这涉及获得   该左值的当前值(而不是旧值的一倍)   i)因此必须在副作用之后进行排序   增量(即更新i

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

  

(2)任务的LHS也是   左值,因此其值评估不涉及获取当前值   价值i;   而这个值的计算是未经测试的w.r.t.该   RHS的值计算,这没有问题

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

  

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

i = expr的值计算仅在您使用结果时才需要,例如int x = (i = expr);(i = expr) = 42;。值计算本身不会修改i

i引起的i = expr表达式=的修改称为=副作用。这个副作用在i = expr的值计算之前排序 - 或者更确切地说 i = expr的值计算在i = expr 中的赋值的副作用之后被排序。

一般来说,表达式操作数的值计算当然是在该表达式的副作用之前排序。

  

但是在其RHS的值计算之后排序,因此在之后   i的上一次更新;没问题。

赋值i = expr副作用在赋值的操作数i(A)和expr的值计算之后排序。

在这种情况下,expr+ - 表达式:expr1 + 1。在对其操作数expr11进行值计算之后,对该表达式的值计算进行排序。

expr1此处为++i。在++i的副作用(++i的修改)(B)后,i的值计算顺序

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


(a)标准根据++expr定义expr += 1expr = expr + 1定义为exprexpr = expr + 1仅评估一次。

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

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


关于i += ++i + 1

  

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

     

自从   +=的LHS仍然是左值(并且其RHS仍然是prvalue),相同   上述推理适用于(1)和(2);   至于   (3)+=运算符的值计算现在必须都获取   当前值i,然后(显然在它之后排序,即使   标准没有明确说明,或者执行   这样的运算符总是会调用未定义的行为)执行   添加RHS并将结果存储回i

我认为问题在于:在i的{​​{1}}的LHS中i +=添加++i + 1需要知道i的值 - 值计算(可以表示加载i的值)。对于由++i执行的修改,该值计算未被排序。这基本上是您在替代说明中所说的,遵循标准i += expr规定的重写 - &gt; i = i + expr。这里,ii + expr的值计算对于expr的值计算未被排序。 你获得UB的地方

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

  

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


i += ++i + 1

的另一种方法
  

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

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

一般来说,商店和&#34;并发&#34; load是一个数据争用,导致Undefined Behavior。


解决附录

  

使用虚构的|||运算符来指定未经测序的计算,可能会尝试将E op= F;(为简单起见将int操作数)定义为等效于{ int& L=E ||| int R=F; L = L + R; },但后面的示例不再具有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;

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

  

该标准不会将复合赋值视为第二类原语,因此不需要单独定义语义。

我会说这是一种冗余。从E1 op = E2E1 = E1 op E2的重写还包括需要哪些表达式类型和值类别(在rhs上,5.17 / 1表示关于lhs的内容),指针类型会发生什么,所需的转换等。可悲的是,关于&#34;关于...&#34;在5.17 / 1中不是在5.17 / 7中作为该等价的例外。

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

一旦我们把这个&#34;关于...&#34;同样在5.17 / 7的例外情况列表中,我并不认为存在矛盾。

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

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

似乎(A)有两种可能的结果,因为f的主体评估是使用ii + f()的值计算不确定地排序的。

另一方面,在(B)中,f()的主体的评估在i的值计算之前被排序,因为+=必须被视为单个操作,f()当然需要在分配+=之前进行评估。

答案 1 :(得分:5)

表达式:

i += ++i + 1

会调用未定义的行为。语言律师方法要求我们返回导致以下内容的缺陷报告:

i = ++i + 1 ;

在C ++ 11中得到很好的定义,即defect report 637. Sequencing rules and example disagree ,它开始说:

  

在1.9 [intro.execution]第16段中,以下表达式为   仍列为未定义行为的示例:

i = ++i + 1;
     

然而,似乎新的测序规则构成了这个表达式   良好定义

报告中使用的逻辑如下:

  1. 在LHS和RHS的值计算(5.17 [expr.ass]第1段)之后,需要对分配副作用进行排序。

  2. LHS(i)是左值,因此其值计算涉及计算i的地址。

  3. 为了计算RHS(++ i + 1)的值,有必要首先对左值表达式++ i进行值计算,然后对结果进行左值到右值的转换。这保证了在计算加法运算之前对递增副作用进行排序,加法运算又在赋值副作用之前进行排序。换句话说,它为此表达式生成一个明确定义的顺序和最终值。

  4. 所以在这个问题中,我们的问题改变了RHS来自:

    ++i + 1
    

    为:

    i + ++i + 1
    

    归因于draft C++11 standard部分5.17 分配和复合赋值运算符,其中包含:

      

    E1 op = E2形式的表达式的行为等同于   E1 = E1操作E2除了E1仅被评估一次。 [...]

    所以现在我们遇到iRHS的计算相对于++i没有排序的情况,因此我们有未定义的行为。这可以从1.9 15 开头说明:

      

    除非另有说明,否则评估各个运营商的操作数   并且个别表达的子表达式没有被排序。 [   注意:在表达式中,在表达式中多次计算   程序的执行,无序和不确定的顺序   不必一致地执行其子表达式的评估   在不同的评估中。 -end note]的计算值   操作符的操作数在值计算之前被排序   运营商的结果。如果对标量对象产生副作用   相对于同一标量的另一个副作用而言,它没有排序   使用相同标量值的对象或值计算   对象,行为未定义。

    显示此功能的实用方法是使用clang来测试代码,该代码会生成以下警告( see it live ):

    warning: unsequenced modification and access to 'i' [-Wunsequenced]
    i += ++i + 1 ;
      ~~ ^
    

    代码:

    int main()
    {
        int i = 0 ;
    
        i += ++i + 1 ;
    }
    

    clang's的{​​{1}}测试套件中的这个明确的测试示例进一步加强了这一点:

     a += ++a; 
    

答案 2 :(得分:1)

是的,是UB!

评估你的表达

i += ++i + 1

按以下步骤进行:

5.17p1(C ++ 11)陈述(强调我的):

  

赋值运算符(=)和复合赋值运算符从右到左分组。所有都需要一个可修改的左值作为左操作数,并返回一个左值操作数的左值。如果左操作数是位字段,则所有情况下的结果都是位字段。 在所有情况下,分配在右和左操作数的值计算之后,以及赋值表达式的值计算之前进行排序。

&#34;值计算&#34;意思?

1.9p12给出答案:

  

访问由volatile glvalue(3.10)指定的对象,修改对象,调用库I / O函数或调用执行任何这些操作的函数都是副作用,这些都是状态的变化。执行环境。表达式(或子表达式)的 评估通常包括值计算(包括确定glvalue评估对象的标识并获取先前分配给对象以进行prvalue评估的值) )并引发副作用。

由于您的代码使用复合赋值运算符,5.17p7告诉我们此运算符的行为:

  

E1 op= E2形式的表达式的行为等同于E1 = E1 op E2 except that E1仅被评估一次。

因此,表达式E1 ( == i)的评估既涉及确定i指定的对象的身份,又涉及左值 - 右值转换以获取存储的值在那个对象。但是对两个操作数E1E2的评估并未相互排序。因此,我们得到未定义的行为,因为E2 ( == ++i + 1)的评估会产生副作用(更新i)。

1.9p15:

  

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


您的问题/评论中的以下陈述似乎是您误解的根源:

  

(2)赋值的LHS也是左值,因此其值评估不涉及获取当前值i

     

获取值可以是prvalue评估的一部分。但是在E + = F中,唯一的prvalue是F,因此获取E的值不是(左值)子表达式E的评估的一部分E

如果表达式是左值或右值,则不会告诉任何有关如何计算此表达式的信息。有些运算符需要左值作为操作数,有些则需要右值。

条款5p8:

  

每当glvalue表达式作为操作符的操作数出现时,该操作符需要该操作数的prvalue,lvalue-to-rvalue(4.1),array-to-pointer(4.2)或function-to-pointer(4.3)应用标准转换以将表达式转换为prvalue。

在简单的分配中,对LHS的评估仅需要确定对象的身份。但是在诸如+=的复合赋值中,LHS必须是可修改的左值,但在这种情况下对LHS的评估包括确定对象的身份和左值到右值的转换。这是转换(它是一个prvalue)的结果,它被添加到RHS评估的结果(也是一个prvalue)中。

&#34;但是在E + = F中,唯一的prvalue是F,因此获取E的值不是(左值)子表达式E&#34;

的评估的一部分

如上所述,这不是真的。在您的示例中,F是一个prvalue表达式,但F也可能是一个左值表达式。在这种情况下,左值到右值的转换也适用于F。上面引用的5.17p7告诉我们,复合赋值运算符的语义是什么。该标准规定E += F行为E = E + F相同,但E仅评估一次。这里,E的评估包括左值到右值的转换,因为二元运算符+要求操作数为rvalues。

答案 3 :(得分:0)

从编译器编写者的角度来看,他们并不关心"i += ++i + 1",因为无论编译器做什么,程序员可能都得不到正确的结果,但他们肯定会得到他们应得的。没有人写这样的代码。编译器作者关心的是

*p += ++(*q) + 1;

代码必须阅读*p*q,将*q增加1,并将*p增加一些计算量。这里编译器编写者关心对读写操作顺序的限制。显然,如果p和q指向不同的对象,则顺序没有区别,但是如果p == q那么它将产生影响。同样,p将与q不同,除非编写代码的程序员是疯了。

通过使代码未定义,该语言允许编译器生成尽可能快的代码,而无需照顾疯狂的程序员。通过定义代码,语言强制编译器生成符合标准的代码,即使在疯狂的情况下,也可能使其运行速度变慢。编译器编写者和理智的程序员都不喜欢这样。

因此,即使行为是在C ++ 11中定义的,使用它也是非常危险的,因为(a)编译器可能不会从C ++ 03行为改变,并且(b)它可能是未定义的出于上述原因,在C ++ 14中的行为。