在C ++ 11中,`i + = ++ i + 1`表现出未定义的行为?
这个问题出现在我阅读(答案)的时候,那么为什么我在C ++ 11中明确地定义了i = ++ i + 1?
我收集到微妙的解释是(1)表达式++i
返回一个左值,但+
将prvalues作为操作数,所以必须执行从左值到左值的转换; 这涉及获得与左值(比的旧值,而不是一个多的当前值i
),并从增量副作用(即,在更新之后,因此必须测序i
)(2)分配的LHS也是因此其价值评估不涉及获取i
的当前价值; 虽然这个值计算与RHS的值计算没有序列关系,但这没有问题。(3)赋值本身的值计算涉及更新i
(再次),但是在其RHS的值计算之后排序,并且因此在先给i
更新; 没问题。
好吧,那里没有UB。 现在我的问题是如果将assigment运算符从=
改为+=
(或类似的运算符)会怎样。
表达式i += ++i + 1
的评估是否会导致未定义的行为?
正如我所看到的,这个标准似乎在这里相互矛盾。 由于+=
的LHS仍然是一个左值(并且其RHS仍然是一个值),就(1)和(2)而言,与上面相同的推理适用; 在+=
上操作数的评估中没有未定义的行为。 对于(3),复合赋值+=
(更确切地说是该操作的副作用;如果需要,其值计算在任何情况下都在其副作用之后排序)现在必须同时获取i
的当前值,然后(在它之后明显排序,即使标准没有明确说明,否则对这些运算符的评估总是会调用未定义的行为)添加RHS并将结果存储回i
。 这些操作都将给予未定义行为,如果他们未测序WRT的副作用++
,但上面认为(的副作用++
中的值计算之前测序+
给人的RHS +=
运营商,在该化合物分配操作之前对该值计算进行排序),但情况并非如此。
但另一方面,该标准还指出, E += F
相当于E = E + F
,只是(左值)E只被评估一次。 现在在我们的例子中,作为左值的i
(这是E
在这里是什么)的值计算不涉及任何需要按照其他动作排序的东西,因此做一次或两次都没有区别; 我们的表达应严格等于E = E + F
但这是问题所在。 很明显,评估i = i + (++i + 1)
会给出未定义的行为! 是什么赋予了? 或者这是标准的缺陷?
添加。 我稍微修改了我的上述讨论,为副作用和价值计算之间的恰当区分做了更多的正义,并使用表达式的“评估”(如同标准一样)来包含两者。 我认为我的主要讯问不仅仅是在这个例子中是否定义了行为,而是如何阅读标准以决定这一点。 值得注意的是,是否应该将E op= F
到E = E op F
的等价性作为复合赋值操作的语义的最终权威(在这种情况下,该例子显然具有UB),或者仅仅作为什么数学运算的指示涉及确定要分配的值(即,由op
标识的值,以化合物赋值运算符的左值至右值转换左边界为左操作数,右边界为右操作数)。 后面的选项使得在这个例子中争论UB更加困难,正如我试图解释的那样。 我承认,有理由认为等价是权威的(因此复合赋值成为一类二级基元,其含义是通过在第一类基元中重写而给出的;因此语言定义将被简化),但是在那里是相当强烈的反对这个论据:
由于“ E
只评估一次”的例外,等值不是绝对的。 请注意,这个例外是必不可少的,以避免在评估E
涉及副作用未定义行为时使用,例如在相当常见的a[i++] += b;
用法。 如果事实我认为没有完全等同的重写来消除复合赋值是可能的; 使用虚构的|||
操作员指定不确定的评估,可能试图定义E op= F;
(为简单起见, int
操作数)等价于{ int& L=E ||| int R=F; L = L + R; }
{ int& L=E ||| int R=F; L = L + R; }
{ int& L=E ||| int R=F; L = L + R; }
,但这个例子不再有UB。 无论如何,这个标准并没有给我们带来任何重要的配方。
该标准不把复合指派看作是不需要单独定义语义的第二类基元。 例如在5.17(强调我的)
赋值运算符(=)和复合赋值运算符全部从右到左组合。 [...] 在所有情况下 ,赋值都在左右操作数的值计算之后,赋值表达式的值计算之前进行排序。 对于一个不确定排序的函数调用, 复合赋值的操作是单个评估 。
如果人们承认复合赋值具有它们自己的语义,那么就会产生这样的观点:他们的评估涉及(除了数学运算)不仅仅是副作用(赋值)和价值评估(赋值后的序列),而是也是取一个LHS(的前一个)值的未命名操作。 这通常在“左值到右值转换”的标题下处理,但这样做很难证明是合理的,因为没有操作符存在将LHS作为右值操作数(尽管扩展中有一个“等同”形式)。 正是这种未命名的操作与++
的副作用潜在的无序关系会导致UB,但这种无序的关系在标准中没有明确说明,因为未命名的操作不是。 UB使用只存在于标准中的操作很难证明UB的合理性。
关于i = ++i + 1
的描述
我收集到微妙的解释
(1)表达式++i
返回一个左值,但+
将prvalues作为操作数,所以必须执行从左值到左值的转换;
可能请参见CWG活动问题1642。
这涉及获得与左值(比的旧值,而不是一个多的当前值i
),并从增量副作用后,因此必须进行测序(即,更新i
)
这里的定序是为增量定义的(间接地,通过+=
,参见(a)): ++
( i
的修改)的副作用在整个表达式++i
的值计算之前被排序。 后者是指计算++i
的结果,而不是加载i
的值。
(2)任务的LHS也是一个左值,因此其值评估不涉及获取i
的当前值; 虽然这个值计算与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
是一个+
expr
: expr1 + 1
。 该表达式的值计算在其操作数expr1
和1
的值计算之后被排序。
这里的expr1
是++i
。 的值计算++i
的副作用之后进行测序++i
(的修改i
)(B)
这就是为什么i = ++i + 1
是安全的 :在(A)中的值计算和(B)中的相同变量的副作用之间存在一连串的顺序。
(a)标准按expr += 1
定义++expr
, expr = expr + 1
, expr
只计算一次。
对于这个expr = expr + 1
,我们因此只有一个expr
值计算。 在整个expr = expr + 1
的值计算之前, =
的副作用被排序,并且在操作数expr
(LHS)和expr + 1
(RHS)的值计算之后进行排序。
这对应于我的声明,对于++expr
,副作用在++expr
的值计算之前排序。
关于i += ++i + 1
i += ++i + 1
的值计算是否涉及未定义的行为?
由于+=
的LHS仍然是一个左值(并且其RHS仍然是一个值),就(1)和(2)而言,与上面相同的推理适用; 至于(3) +=
运算符的值计算现在必须同时提取i
的当前值,然后(在它后面显然排序,即使标准没有明确说明,否则这些运算符的执行总是会调用未定义的行为)执行RHS的添加并将结果存储回i
。
我觉得这里的问题:加入i
在的LHS i +=
到结果++i + 1
需要知道的价值i
-一个值计算(这可能意味着装载的价值i
)。 对于由++i
执行的修改,该值计算是不确定的。 这基本上就是你在替代描述中所说的,遵循标准i += expr
- > i = i + expr
强制的重写。 这里, i + expr
的i
的值计算相对于expr
的值计算是expr
。 这就是你获得UB的地方 。
请注意,值计算可能有两个结果:对象的“地址”或对象的值。 在表达式i = 42
,lhs“的值计算产生i
的地址” 也就是说,编译器需要找出存储rhs的位置(在抽象机器的可观察行为规则下)。 在表达式i + 42
,的值计算i
产生的值。 在上面的段落中,我指的是第二种,因此[intro.execution] p15适用:
如果对标量对象的副作用相对于同一标量对象的另一副作用或使用同一标量对象的值进行值计算而言是不确定的,则行为是未定义的。
i += ++i + 1
另一种方法
现在+=
运算符的值计算必须同时获取i
的当前值, 然后执行RHS的加法运算
RHS是++i + 1
。 计算这个表达式的结果(值计算)与LHS的i
值计算没有关系。 所以在这句话中这个词是误导性的:当然,它必须首先加载i
,然后将RHS的结果添加到它。 但是在RHS的副作用和计算LHS的价值之间没有秩序。 例如,你可以得到的LHS无论是旧的或新的价值i
,由RHS进行修改。
一般来说,商店和“并发”加载是数据竞争,导致未定义的行为。
解决附录
使用虚构的|||
操作员指定不确定的评估,可能试图定义E op= F;
(为简单起见,int操作数)等价于{ int& L=E ||| int R=F; L = L + R; }
{ int& L=E ||| int R=F; L = L + R; }
{ int& L=E ||| int R=F; L = L + R; }
,但这个例子不再有UB。
让E
是i
, F
是++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
=> UB的左值到右值转换无序。
该标准不把复合指派看作是不需要单独定义语义的第二类基元。
我会说这是一种冗余。 从E1 op = E2
到E1 = E1 op E2
的重写还包括哪些表达式类型和值类别是必需的(在rhs上,5.17 / 1说明了关于lhs的一些事情),指针类型会发生什么,需要的转换等等。令人悲伤的是,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
在i + f()
另一方面,在(B)中, f()
的主体的评价在i
的值计算之前被排序,因为+=
必须被看作单个操作,并且f()
当然需要在赋值+=
。
表达方式:
i += ++i + 1
会调用未定义的行为。 语言律师方法要求我们回到缺陷报告,结果如下:
i = ++i + 1 ;
在C ++ 11中定义良好,这是缺陷报告637.排序规则和示例不一致,它开始说:
在1.9 [intro.execution]第16段中,以下表达式仍被列为未定义行为的示例:
i = ++i + 1;
但是,新的排序规则似乎使这个表达式得到了很好的定义
报告中使用的逻辑如下:
在LHS和RHS(5.17 [expr.ass]段落1)的值计算之后,分配副作用需要排序。
LHS(i)是一个左值,所以它的值计算包括计算i的地址。
为了计算RHS(++ i + 1)的值,必须首先对左值表达式++ i进行值计算,然后对结果进行左值到右值转换。 这保证了增量副作用在计算加法运算之前被排序,而加法运算又是在分配副作用之前排序的。 换句话说,它为这个表达式产生一个明确定义的顺序和最终值。
所以在这个问题中,我们的问题改变了RHS
:
++i + 1
至:
i + ++i + 1
由于草稿C ++ 11标准章节5.17
赋值和复合赋值操作符说:
形式E1 op = E2的表达式的行为等同于E1 = E1 op E2,除了E1仅评估一次。 [...]
因此,现在我们有一种情况, i
RHS
的i
的计算没有相对于++i
排序,因此我们有未定义的行为。 这从第1.9
节第15段开始说:
除非另有说明,否则对个别操作符和个别表达式的操作数的评估是不确定的。 [注意:在一个程序执行过程中多次评估的表达式中,对其子表达式的无序和不定序评估不需要在不同的评估中一致地执行。 - 注意]运算符结果的值计算之前,运算符操作数的值计算是排序的。 如果对标量对象的副作用相对于同一标量对象的另一副作用或使用同一标量对象的值进行值计算而言是不确定的,则行为是未定义的。
显示这一点的实用方法是使用clang
来测试代码,该代码会生成以下警告(请参阅实况):
warning: unsequenced modification and access to 'i' [-Wunsequenced]
i += ++i + 1 ;
~~ ^
对于此代码:
int main()
{
int i = 0 ;
i += ++i + 1 ;
}
在clang's
测试套件中,这个明确的测试例子进一步支持了这个测试 - 时序:
a += ++a;
是的,这是UB!
评估你的表情
i += ++i + 1
按以下步骤进行:
5.17p1(C ++ 11)状态(重点介绍):
赋值运算符(=)和复合赋值运算符全部从右到左组合。 所有需要一个可修改的左值作为它们的左操作数并返回一个左值指向左操作数。 如果左操作数是位域,则所有情况下的结果都是位域。 在所有情况下,赋值都在左右操作数的值计算之后,赋值表达式的值计算之前进行排序。
“价值计算”是什么意思?
1.9p12给出了答案:
访问由volatile glvalue(3.10)指定的对象,修改对象,调用库I / O函数或调用执行任何这些操作的函数都是副作用,这些副作用是执行环境状态的变化。 对表达式(或子表达式)的评估通常包括值计算(包括确定对于glvalue评估的对象的身份并获取先前分配给对象以用于评估值)和副作用的启动。
由于您的代码使用复合赋值运算符,因此5.17p7告诉我们,该运算符的行为如何:
形式E1 op= E2
的表达式的行为等同于E1 = E1 op E2 except that
E1仅评估一次。
因此,对表达式E1 ( == i)
的评估涉及确定由i
指定的对象的身份和左值到右值的转换,以获取存储在该对象中的值。 但是对两个操作数E1
和E2
的评估没有相互排序。 因此,我们得到未定义的行为,因为E2 ( == ++i + 1)
的评估会引发副作用(更新i
)。
1.9p15:
... 如果对标量对象的副作用不是相对于同一个标量对象的另一个副作用或使用相同标量对象的值进行值计算,那么行为是未定义的。
您的问题/评论中的以下陈述似乎是您误解的根源:
(2)任务的LHS也是一个左值,因此其值评估不涉及获取i
的当前值
获取价值可以成为估价评估的一部分。 但在E + = F中,唯一的前值是F,因此取E的值不是评估(左值)子表达式E的一部分
如果一个表达式是一个左值,或者右值没有告诉任何关于如何评估这个表达式的内容。 一些操作符需要左值作为操作数,而另一些则需要右值。
第5p8条:
每当一个glvalue表达式作为一个操作符的操作数出现,该操作数需要该操作数的一个值时,左值到右值(4.1),数组到指针(4.2)或函数到指针(4.3)的标准转换是用于将表达式转换为前值。
在简单的任务中,LHS的评估只需要确定对象的身份。 但是在一个复合赋值中,例如+=
LHS必须是一个可修改的左值,但在这种情况下LHS的评估由确定对象的身份和左值到右值的转换组成。 这是这种转换(这是一种价值)的结果,它被加到了RHS评估的结果(也是一个价值)上。
“但是在E + = F中,唯一的价值是F,所以提取E的价值不是评价(左值)子表达式E”
这是不正确的,因为我上面解释过。 在你的例子中, F
是一个prvalue表达式,但F
也可能是一个左值表达式。 在这种情况下,左值到右值的转换也适用于F
上面引用的5.17p7告诉我们,复合赋值运算符的语义是什么。 该标准规定的行为 E += F
相同的E = E + F
但E
只计算一次。 在这里, E
的评估包括左值到右值的转换,因为二元运算符+
要求它的操作数是右值。
上一篇: In C++11, does `i += ++i + 1` exhibit undefined behavior?