Mar*_*wen 47 c++ undefined-behavior language-lawyer c++11
在我阅读的过程中出现了这个问题(答案) 那么为什么i = ++ i + 1在C++ 11中定义明确?
我认为,微妙的解释是(1)表达式++i
返回一个左值但是+
将prvalues作为操作数,因此必须执行从左值到右值的转换; 这涉及获得该左值的当前值(而不是旧值的一个以上i
),因此必须在增量的副作用(即更新)之后进行排序i
(2)赋值的LHS也是左值,所以它的价值评估不涉及取得当前价值i
; 虽然这个值计算在RHS的值计算中是不可测的,但这没有问题.(3)赋值本身的值计算涉及更新i
(再次),但是在其RHS的值计算之后排序,因此在变换之后更新到i
; 没问题.
很好,所以那里没有UB.现在我的问题是如果将分配运算符更改=
为+=
(或类似的运算符).
表达式的评估是否会
i += ++i + 1
导致未定义的行为?
在我看来,标准似乎在这里自相矛盾.由于LHS +=
仍然是左值(并且其RHS仍然是prvalue),所以与上述相同的推理适用于(1)和(2); 在操作数的评估中没有未定义的行为+=
.至于(3),复合赋值的运算+=
(更确切地说是该运算的副作用;它的值计算,如果需要,在任何情况下在其副作用之后排序)现在必须同时获取当前值i
,然后(显然在它之后排序,即使标准没有明确说明,或者对这些运算符的评估总是会调用未定义的行为)添加RHS并将结果存回i
.如果它们的副作用没有排序,这两个操作都会给出未定义的行为++
,但正如上面所论述的那样(在给出运算符的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
左侧操作数的左值操作数和左侧操作数的RHS作为左值操作数的左值和左值转换后的L值表示的值.正如我试图解释的那样,后一种选择使得在这个例子中为UB争论更加困难.我承认,使等价具有权威性是很诱人的(因此复合赋值成为一种二级基元,其含义是通过用一等基元重写来给出的;因此语言定义会被简化),但那里反对这是相当强烈的论据:
等价不是绝对的,因为" E
仅评估一次"异常.请注意,此异常对于避免在评估E
涉及副作用未定义行为时进行任何使用至关重要,例如在相当常见的a[i++] += b;
用法中.事实上,我认为没有绝对等同的重写来消除复合分配是可能的; 使用虚拟|||
运算符指定未序列的计算,可能会尝试定义E op= F;
(int
为简单起见,操作数)等效{ int& L=E ||| int R=F; L = L + R; }
,但该示例不再具有UB.在任何情况下,标准都没有给我们重新配方.
该标准不会将复合赋值视为第二类原语,因此不需要单独定义语义.例如在5.17(强调我的)
赋值运算符(=)和复合赋值运算符都是从右到左分组.[...] 在所有情况下,赋值在右和左操作数的值计算之后,以及赋值表达式的值计算之前排序.对于不确定序列的函数调用,复合赋值的操作是单个评估.
如果一个人承认复合作业具有自己的语义,那么就会产生这样的观点:他们的评价不仅仅涉及副作用(作业)和价值评估(作业后的顺序),而是(除了数学运算之外),但是也是一个获取LHS(先前)值的未命名操作.这通常在"左值到右值转换"的标题下处理,但这样做很难证明,因为没有操作符存在将LHS 作为右值操作数(尽管在扩展中有一个) "等同"形式).正是这种未命名的操作,其副作用与潜在的无序关系++
会导致UB,但这种无序的关系在标准中没有明确说明,因为未命名的操作不是.使用一种只存在于标准中的操作很难证明UB的合理性.
dyp*_*dyp 16
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
这种情况下是一个+
-expression: expr1 + 1
.该表达式的值计算在其操作数expr1
和值的计算之后进行排序1
.
这expr1
是++i
.在(修改)(B)++i
的副作用之后对值计算进行排序++i
i
这就是为什么i = ++i + 1
是安全的:在(A)中的值计算和(B)中的相同变量的副作用之间存在一系列顺序.
(a)中所述的标准定义++expr
在以下方面expr += 1
被定义为,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仍然是prvalue),所以与上述相同的推理适用于(1)和(2); 至于(3)运算+=
符的值计算现在必须都获取当前值i
,然后(显然在它之后排序,即使标准没有如此明确地说,或者否则这些运算符的执行总是会调用未定义的行为)执行RHS的添加并将结果存回i
.
我认为这就是问题:i
在LHS中i +=
添加结果++i + 1
需要知道i
- 值计算的值(可以意味着加载值i
).对于由其执行的修改,该值计算未被排序++i
.这基本上就是您在替代描述中所说的,遵循标准规定的重写i += expr
- > i = i + expr
.在此,的值计算i
内i + expr
被未测序相对于的值计算expr
.这就是你获得UB的地方.
请注意,值计算可以有两个结果:对象的"地址"或对象的值.在表达式中i = 42
,lhs的值计算"产生地址" i
; 也就是说,编译器需要找出存储rhs的位置(在抽象机器的可观察行为规则下).在表达式中i + 42
,值计算i
产生值.在上一段中,我指的是第二种,因此[intro.execution] p15适用:
如果对标量对象的副作用相对于同一标量对象的另一个副作用或使用相同标量对象的值进行的值计算未被排序,则行为未定义.
i += ++i + 1
该值计算
+=
运营商现在都必须获取的当前值i
,并再 [...]执行加法等式右边的
RHS正在++i + 1
.计算该表达式的结果(值计算)对于i
来自LHS 的值计算是不可测的.所以这个词,然后在这句话是一种误导:当然,它必须首先加载i
,然后RHS的结果添加到它.但是,RHS的副作用和价值计算之间没有任何顺序来获得LHS的价值.例如,您可以为LHS获取i
由RHS修改的旧值或新值.
通常,存储和"并发"加载是数据争用,这会导致未定义的行为.
使用虚构
|||
运算符来指定未序列的计算,可能会尝试将E op= F;
(为简单起见将int操作数)定义为等效{ 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;
Run Code Online (Sandbox Code Playgroud)
另一方面,为 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;
Run Code Online (Sandbox Code Playgroud)
这是为了表示我对排序保证的理解.注意,,
操作员在RHS之前对LHS的所有值计算和副作用进行排序.括号不影响排序.在第二种情况下,i += ++i
我们对未i
测序的wrt进行了修改,使得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)
}
Run Code Online (Sandbox Code Playgroud)
似乎(A)有两种可能的结果,因为身体的评估f
是用i
in 的值计算不确定地排序的i + f()
.
另一方面,在(B)中,对身体的评价f()
在值计算之前被排序i
,因为+=
必须被视为单个操作,并且f()
当然需要在分配之前进行评估+=
.
表达方式:
i += ++i + 1
Run Code Online (Sandbox Code Playgroud)
会调用未定义的行为.语言律师方法要求我们返回导致以下结果的缺陷报告:
i = ++i + 1 ;
Run Code Online (Sandbox Code Playgroud)
在C++ 11中得到了很好的定义,这是缺陷报告637.排序规则和示例不一致,它开始说:
在1.9 [intro.execution]第16段中,以下表达式仍作为未定义行为的示例列出:
Run Code Online (Sandbox Code Playgroud)i = ++i + 1;
但是,似乎新的测序规则使这个表达式定义明确
报告中使用的逻辑如下:
在LHS和RHS(5.17 [expr.ass]第1段)的值计算之后,需要对分配副作用进行排序.
LHS(i)是左值,因此其值计算涉及计算i的地址.
为了计算RHS(++ i + 1)的值,有必要首先对左值表达式++ i进行值计算,然后对结果进行左值到右值的转换.这保证了在计算加法运算之前对递增副作用进行排序,加法运算又在赋值副作用之前进行排序.换句话说,它为此表达式生成明确定义的顺序和最终值.
所以在这个问题中我们的问题改变了RHS
:
++i + 1
Run Code Online (Sandbox Code Playgroud)
至:
i + ++i + 1
Run Code Online (Sandbox Code Playgroud)
由于草案C++ 11标准部分5.17
赋值和复合赋值运算符,它们表示:
形式E1 op = E2的表达式的行为等同于E1 = E1 op E2,除了E1仅被评估一次.[...]
所以,现在我们有一个情况的计算i
中RHS
并不相对于测序++i
,所以我们再有不确定的操作.这是根据1.9
第15段说的:
除非另有说明,否则对单个运算符的操作数和单个表达式的子表达式的评估是不确定的.[注意:在程序执行期间不止一次评估的表达式中,不需要在不同的评估中一致地执行对其子表达式的未序列和不确定顺序的评估.-end note]运算符操作数的值计算在运算符结果的值计算之前排序.如果对标量对象的副作用相对于同一标量对象的另一个副作用或使用相同标量对象的值进行的值计算未被排序,则行为未定义.
显示这一点的实用方法是用来clang
测试代码,它会生成以下警告(请参见实时):
warning: unsequenced modification and access to 'i' [-Wunsequenced]
i += ++i + 1 ;
~~ ^
Run Code Online (Sandbox Code Playgroud)
对于此代码:
int main()
{
int i = 0 ;
i += ++i + 1 ;
}
Run Code Online (Sandbox Code Playgroud)
clang's
对于-Wunsequenced的测试套件中的这个显式测试示例进一步支持了这一点:
a += ++a;
Run Code Online (Sandbox Code Playgroud)
当然,正如我在问题中指出的那样,可以给出导致 UB 的论点,并且在迄今为止给出的答案中已经重复了这一点。然而,这涉及对 5.17:7 的严格解读,这既是自相矛盾的,又与 5.17:1 中关于复合赋值的明确陈述相矛盾。阅读较弱的 5.17:7 后,矛盾就消失了,UB 的论证也消失了。因此,我的结论是,这里既没有 UB,也没有明确定义的行为,但标准的文本不一致,应该修改以明确哪种阅读占主导地位(我想这意味着缺陷报告应该是书面)。当然,人们可能会在这里引用标准中的后备条款(1.3.24 中的注释),即标准未能[明确且自洽地]定义行为的评估是未定义行为,但这将有任何用处复合赋值(包括前缀递增/递减运算符)到 UB 中,这可能会吸引某些实现者,但肯定不会吸引程序员。
\n\n让我不去争论给定的问题,而是提出一个稍微修改过的例子,更清楚地揭示出不一致之处。假设已经定义了
\n\nint& f (int& a) { return a; }\n
Run Code Online (Sandbox Code Playgroud)\n\n一个不执行任何操作并返回其(左值)参数的函数。现在将示例修改为
\n\nn += f(++n) + 1;\n
Run Code Online (Sandbox Code Playgroud)\n\n请注意,虽然标准中给出了一些关于函数调用顺序的额外条件,但乍一看这似乎不会影响示例,因为函数调用根本没有副作用(甚至在函数内部也没有副作用),因为增量发生在 的参数表达式中f
,其计算不受这些额外条件的影响。事实上,让我们应用未定义行为的关键论证 (CAUB),即 5.17:7,它表示这种复合赋值的行为相当于(在本例中)
n = n + f(++n) + 1;\n
Run Code Online (Sandbox Code Playgroud)\n\n只不过n
只计算一次(这里没有区别的例外)。我刚刚写的语句的评估清楚地具有 UBn
( RHS 中第一个(纯右值)的值计算是无序的++
,因为操作的副作用涉及相同的标量对象 (1.9:15),并且您\死的)。
所以 的评估n += f(++n) + 1
有未定义的行为,对吧?错误的!请阅读 5.17:1
\n\n\n对于不确定顺序的函数调用,复合赋值操作是一次计算。[注意:因此,函数调用不应干预左值到右值的转换和与任何单个复合赋值运算符相关的副作用。\xe2\x80\x94尾注]
\n
这种语言远没有我希望的那么精确,但我认为假设“不确定顺序”应该意味着“相对于复合赋值操作”并不夸张。(我知道是非规范的)注释清楚地表明左值到右值的转换是复合赋值操作的一部分。现在是关于 的f
复合赋值操作的不确定顺序调用+=
?我不确定,因为“顺序”关系是为单个值计算和副作用定义的,而不是对运算符的完整评估,这可能涉及两者。事实上,复合赋值运算符的求值涉及三项:其左操作数的左值到右值的转换、副作用(赋值本身)以及复合赋值的值计算(在副作用之后排序) ,并将原始左操作数作为左值返回)。请注意,除了上面引用的注释之外,标准中从未明确提及左值到右值转换的存在;特别是,该标准根本没有就其相对于其他评估的排序做出任何(其他)声明。很明显,在示例中, 的调用f
是在的副作用和值计算之前+=
排序的(因为调用发生在 的右操作数的值计算中+=
),但它可能相对于左值是不确定排序的 -到右值转换部分。我从我的问题中回忆起,由于 的左操作数+=
是左值(并且必然如此),因此不能将左值到右值的转换解释为作为左操作数的值计算的一部分而发生。
然而,根据排中律原则,对 的调用f
必须要么相对于 的复合赋值操作是不确定排序的+=
,要么是非不确定排序的;在后一种情况下,它必须在它之前排序,因为它不可能在它之后排序(调用f
在 的副作用之前排序+=
,并且关系是反对称的)。因此,首先假设它的操作顺序是不确定的。然后引用的子句说 wrt 的求值调用f
是+=
单个操作,注释解释说这意味着该调用不应干预左值到右值的转换和与 相关的副作用+=
;它应该在两者之前或两者之后排序。但不可能在副作用之后进行测序,因此应该在两者之前进行测序。++
这使得(通过传递性)在左值到右值转换之前排序的副作用退出 UB。接下来假设 的调用f
在 的操作之前排序+=
。然后在左值到右值转换之前对其进行特别排序,并且再次通过传递性, ; 的副作用也是如此++
。这个分支也没有UB。
结论:5.17:1 与 5.17:7 相矛盾,如果 5.17:7 被认为 (CAUB) 对于 1.9:15 未排序评估产生的 UB 问题具有规范性。正如我所说,CAUB 也是自相矛盾的(通过问题中指出的论点),但这个答案太长了,所以我现在就先到此为止。
\n\n为了理解标准对这些问题的描述,我区分了文本难以解释的三个方面:它们的本质都是文本对于其陈述所指的模型不够清楚。(我引用了编号项目末尾的文本,因为我不知道在引用后恢复编号项目的标记)
\n\n5.17:7 的文字表面上很简单,虽然其意图很容易理解,但在应用于困难的情况时却很难让我们有把握。它做出了全面的声明(显然在所有方面都是等效的行为),但其应用受到例外子句的阻碍。E1 = E1
如果op 的行为E2
未定义怎么办?那么E1
op = E2
也应该如此。但如果 UB 是由于在 opE1
中被评估两次怎么办?那么评估op大概应该不是 UB,但如果是的话,那么定义为什么呢?这就像说“第二个双胞胎的青春期与第一个一模一样,只是他没有在分娩时死亡”。坦率地说,我认为这篇文章自 C 版本以来几乎没有发展“形式的复合赋值与简单赋值表达式的不同之处仅在于左值仅计算一次。” 可能会进行调整以适应标准的变化。E1 = E1
E2
E1
= E2
E1 op = E2
E1 = E1 op E2
E1
\n\n
E1
(5.17) 7 op 形式的表达式的行为= E2
等同于\nE1 = E1
opE2
,只不过E1
它只计算一次。[...]
目前还不清楚定义“顺序”关系的动作(评估)到底是什么。据说(1.9:12)表达式的求值包括值计算和副作用的启动。虽然这似乎是说一个评估可能有多个(原子)组件,但顺序关系实际上主要是为各个组件定义的(例如在 1.9:14,15 中),因此最好将其理解为以下概念: “评估”包括价值计算和副作用(的启动)。然而,在某些情况下,“顺序”关系是为语句表达式 (1.9:15) 的(整个)执行或函数调用 (5.17:1) 定义的,即使 1.9:15 中的段落避免了后者通过直接引用被调用函数体内的执行来实现。
\n\n\n(1.9) 12表达式(或子表达式)的计算通常包括值计算 (...) 和副作用的启动。[...] 13之前排序是单个线程执行的计算之间的不对称、传递、成对关系 [...] 14 与完整表达式相关的每个值计算和副作用都在每个值计算之前排序,并且与下一个要评估的完整表达式相关的副作用。[...] 15 调用函数时(无论该函数是否内联),与任何参数表达式或指定被调用函数的后缀表达式关联的每个值计算和副作用\n 在执行前都会\n 排序被调用函数体内的每个表达式或语句。[...] 调用函数中的每个计算(包括其他函数调用)... 相对于被调用函数的执行而言是不确定地排序的 [...] (5.2.6, 5.17) 1 ...对于不确定顺序的函数调用,...
\n
文本应该更清楚地承认,与简单赋值相比,复合赋值涉及获取先前分配给其左操作数的值的操作;此操作类似于左值到右值的转换,但不会作为左操作数的值计算的一部分发生,因为它不是纯右值;事实上,1.9:12 只承认纯右值评估的这种行为是一个问题。特别是,文本应该更清楚地说明为该操作给出了哪些“顺序”关系(如果有)。
\n\n\n(1.9) 12表达式的求值...包括...值计算(包括确定用于左值求值的对象的身份以及获取先前分配给对象用于纯右值求值的值)
\n
第二点与我们的具体问题最不直接相关,我认为只需选择一个明确的观点并重新表述似乎表明不同观点的段落即可解决它。鉴于旧序列点和现在的“序列”关系的主要目的之一是明确后缀增量运算符的副作用对于在该运算符的值计算之后排序的操作来说是未排序的(因此给出例如i = i++
UB),观点必须是个体值计算和个体副作用(的启动)是可以定义“之前排序”的“评估”。出于务实的原因,我还将包括另外两种(琐碎的)“评估”:函数入口(以便 1.9:15 的语言可以简化为:“当调用函数时......,每个值计算和相关的副作用其任何参数表达式,或指定被调用函数的后缀表达式,在该函数的进入之前排序”)和函数退出(以便函数体中的任何操作通过传递性在需要函数值的任何操作之前排序;这曾经是由序列点保证的,但 C++11 标准似乎已经失去了这样的保证;这可能会导致调用一个return i++;
可能以 UB 结尾的函数,而这并不是本意的,并且过去是安全的)。然后,我们还可以清楚函数调用的“不确定顺序”关系:对于每个函数调用,以及不是(直接或间接)评估该调用的一部分的每个评估,该评估应按顺序进行(之前或之后)对于该函数调用的进入和退出,并且在两种情况下都应具有相同的关系(因此,特别是此类外部操作不能在函数进入之后但在函数退出之前排序,这在单个线程中显然是可取的)。
现在要解决第 1 点和第 3 点,我可以看到两条路径(每条路径都影响这两个点),这对我们示例的已定义或未定义行为产生不同的后果:
\n\n复合运算有两个常用操作数,一个左值左操作数和一个纯右值右操作数。为了解决 3. 的不明确之处,1.9:12 中包含了获取先前分配给对象的值也可能发生在复合赋值中(而不仅仅是纯右值评估)。通过将 5.17:7 更改为
\n\n\n\n\n在复合赋值op
\n=
中,获取先前分配给左操作数引用的对象的值,并应用运算符op将此值作为左操作数和op的右操作数=
的右操作数作为右操作数,结果值替换左操作数引用的对象。
(这给出了两个评估,即获取和副作用;第三个评估是复合运算符的简单值计算,在其他两个评估之后排序。)
\n\n为了清楚起见,在 1.9:15 中明确指出,操作数中的值计算在与运算符关联的所有值计算(而不仅仅是运算符结果的值计算)之前进行排序,这确保了在获取左值操作数之前对左值左操作数的求值进行排序。 value(否则很难想象),并且还在该获取之前对右操作数的值计算进行排序,因此在我们的示例中排除了 UB。在此过程中,我认为没有理由不在出现任何副作用之前对操作数中的值计算进行排序之前对操作数中的值计算进行排序(因为它们显然必须这样做);这将使 5.17:1 中的(复合)赋值明确提及这一点是多余的。另一方面,请注意,复合赋值中的值获取是在其副作用之前排序的。
\n\n为了使复合赋值中的获取相对于正确操作数的值计算而言是无序的,使我们的示例成为 UB,最清晰的方法似乎是为复合运算符提供隐式第三个(中间)操作数,即纯右值,不是用单独的表达式表示,而是通过左操作数的左值到右值转换获得(这种三操作数性质对应于复合赋值的扩展形式,但是通过从左操作数获得中间操作数,可以保证该值是从存储结果的同一对象中获取的,这是当前公式中通过“除了仅E1
评估一次”子句模糊且隐式给出的关键保证。与之前解决方案的区别在于,获取现在是真正的左值到右值转换(因为中间操作数是纯右值),并且作为复合赋值操作数的值计算的一部分执行执行,这使得它很自然与右操作数的值计算不排序。应该在某处(在描述此隐式操作数的新子句中)声明左操作数的值计算在左值到右值转换之前进行排序(显然必须如此)。现在 1.9:12 可以保留原样,我建议代替 5.17:7
\n\n\n在具有左操作数(左值)、中操作数和右操作数(均为纯右值)的复合赋值操作中,运算符op分别应用于左操作数和 右操作数,结果值替换由
\n=
a
b
c
b
c
a
。
(这给出了一个评估,即副作用,第二个评估是复合运算符的平凡值计算,在其之后排序。)
\n\n先前解决方案中建议的对 1.9:15 和 5.17:1 的仍然适用的更改仍然适用,但不会给出我们最初的示例定义的行为。然而,这个答案顶部的修改示例仍然具有定义的行为,除非第 5.17:1 部分“复合赋值是单个操作”被废弃或修改(5.2.6 中有一个类似的段落用于后缀增量/减量) 。这些段落的存在表明,在单个复合赋值或后缀增量/减量中分离 fecth 和 store 操作并不是编写当前标准(并通过扩展创建我们的示例 UB)的人的意图,但这当然是仅仅是猜测。
\n