【发布时间】:2021-01-19 13:02:46
【问题描述】:
最初,我提出了一个更复杂的例子,这个例子是@n提出的。 '代词' m。在一个现已删除的答案中。但是问题变得太长了,如果您有兴趣,请查看编辑历史记录。
以下程序在 C++17 中有明确定义的行为吗?
int main()
{
int a=5;
(a += 1) += a;
return a;
}
我相信这个表达式是定义良好的,并且像这样评估:
- 右侧
a被评估为5。 - 右侧没有副作用。
- 左侧被评估为对
a的引用,a += 1肯定是明确定义的。 - 左侧的副作用被执行,生成
a==6。 - 评估分配,将
a的当前值加 5,使其变为 11。
标准的相关章节:
如果满足条件,则称表达式 X 在表达式 Y 之前排序 与 表达式 X 在每个值计算之前排序,并且每个 与表达式 Y 相关的副作用。
[expr.ass]/1(强调我的):
赋值运算符 (=) 和复合赋值运算符 all 从右到左分组。都需要一个可修改的左值作为左值 操作数;它们的结果是一个引用左操作数的左值。这 如果左操作数是位域,则结果在所有情况下都是位域。 在所有情况下,赋值都是在值计算之后排序的 左右操作数,在计算值之前 赋值表达式。右操作数在 左操作数。对于不确定顺序的函数调用,复合赋值的操作是单次求值。
措辞最初来自被接受的论文P0145R3。
现在,我觉得这第二部分有些模棱两可,甚至矛盾。
右操作数在左操作数之前排序。
加上sequenced before的定义强烈暗示了副作用的排序,然而前一句:
在所有情况下,赋值都是在值计算之后排序的 左右操作数,在计算值之前 赋值表达式
仅在值计算后显式排序赋值,而不是它们的副作用。因此允许这种行为:
- 右侧
a被评估为5。 - 左侧被评估为
a的引用,a += 1肯定是明确定义的。 - 评估分配,将
a的当前值加 5,使其变为 10。 - 左侧的副作用被执行,如果使用旧值甚至是副作用,则生成
a==11或什至可能是6。
但是这种排序显然违反了sequenced before的定义,因为左操作数的副作用发生在右操作数的值计算之后。因此,左操作数没有排在使上述句子紫罗兰色的右操作数之后。 不,我做错了。这是允许的行为,对吗? IE。分配可以交错左右评估。或者可以在两次全面评估之后完成。
Running the code gcc 输出 12,clang 11。此外,gcc 会发出警告
<source>: In function 'int main()':
<source>:4:8: warning: operation on 'a' may be undefined [-Wsequence-point]
4 | (a += 1) += a;
| ~~~^~~~~
我在阅读汇编方面很糟糕,也许有人至少可以重写 gcc 是如何达到 12 的? (a += 1), a+=a 有效,但这似乎是错误的。
好吧,仔细想想,右侧也确实评估了对 a 的引用,而不仅仅是值 5。所以 Gcc 仍然可能是正确的,在这种情况下,clang 可能是错误的。
【问题讨论】:
-
读取程序集无助于调试编译时间计算,因为编译器会输出这些结果。
-
为什么要坚持在同一行使用
*=和+=?把它分成两行。即使它被正确排序,它仍然对人类来说是不可读的(或者如果你想要一个更温和的陈述,则会增加认知负担) -
@Jeffrey 我明确表示我不关心此类代码的可读性,我想知道它为什么不起作用。两行折叠表达式怎么写?
-
好的,但现在问题相当复杂。如果您确定可以将问题简化为
(a += 1) += a;,则删除其他所有内容。否则,提出一个新问题来询问这个问题,如果你愿意,可以链接到这个问题作为动机。 (请注意,这个问题已经在要求两件事,一个解释和一个修复。这可能是区分它们的好方法)。 -
是的,现在清楚多了。是的,如果这不是 UB,那么单独发布原始问题是个好主意。另外,我稍微调整了一下标签。
标签: c++ c++17 language-lawyer sequence-points