问题描述
最初,我提供了一个更复杂的示例,该示例由@n提出。代词现已删除的答案中。但是问题变得太长了,如果您有兴趣,请参阅编辑历史记录。
以下程序在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(重点是我):
赋值运算符(=)和复合赋值运算符全部 从右到左分组。所有人都需要一个可修改的左值 操作数它们的结果是一个指向左操作数的左值。的 如果左操作数是一个位字段,则在所有情况下结果都是一个位字段。 在所有情况下,分配都在值计算之后进行排序 左右操作数的和,在计算值之前 赋值表达式。右操作数在 左操作数。对于不确定顺序的函数调用,复合赋值的操作是单个求值。
措辞最初来自已接受的论文P0145R3。
现在,我觉得在第二部分中有些含糊甚至是矛盾。
右操作数在左操作数之前排序。
连同之前排序的的定义强烈暗示了副作用的排序,而前一句话是
在所有情况下,分配都在值计算之后进行排序 左右操作数的和,在计算值之前 赋值表达式
仅在值计算后显式地对赋值进行排序,而不是其副作用。因此允许这种行为:
- 右侧
a
的值为5。 - 左侧评估为对
a
的引用,a += 1
的定义明确。 - 对赋值进行评估,将
a
的当前值加5,使其变为10。 - 执行左侧副作用,如果甚至将旧值用于副作用,也会产生
a==11
甚至是6
。
但是此顺序明显违反了先后的定义,因为左操作数的副作用发生在右操作数的值计算之后。因此,左操作数没有在右操作数之后排序,从而使上述句子变得紫罗兰色。这是允许的行为,对吗?即分配可以交错左右评估。也可以在两次完整评估后完成。
Running the code gcc输出12,clang11。此外,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可能是错误的。
解决方法
为了更好地了解实际执行的操作,让我们尝试使用我们自己的类型来模仿它,并添加一些打印输出:
class Number {
int num = 0;
public:
Number(int n): num(n) {}
Number operator+=(int i) {
std::cout << "+=(int) for *this = " << num
<< " and int = " << i << std::endl;
num += i;
return *this;
}
Number& operator+=(Number n) {
std::cout << "+=(Number) for *this = " << num
<< " and Number = " << n << std::endl;
num += n.num;
return *this;
}
operator int() const {
return num;
}
};
然后当我们运行时:
Number a {5};
(a += 1) += a;
std::cout << "result: " << a << std::endl;
We get different results with gcc and clang(并且没有任何警告!)。
gcc:
+=(int) for *this = 5 and int = 1
+=(Number) for *this = 6 and Number = 6
result: 12
叮当声:
+=(int) for *this = 5 and int = 1
+=(Number) for *this = 6 and Number = 5
result: 11
与问题中的整数结果相同。 尽管故事并不完全相同:内置分配具有自己的排序规则,因为与作为函数调用的重载运算符相对,相似性仍然很有趣。
似乎 gcc 保留右侧作为参考,并在调用+ =时将其转换为值,而 clang 另一方面将右侧优先考虑一个值。
下一步将是将复制构造函数添加到我们的Number类中,以在将引用转换为值时准确地进行操作。 Doing that的结果是调用复制构造函数作为第一个操作(由clang和gcc进行,),并且两者的结果相同:11 。
似乎 gcc 会延迟对值转换的引用(既在内置分配中,也具有用户定义的类型,而没有用户定义的副本构造函数)。它与C ++ 17定义的排序一致吗?在我看来,这似乎是一个gcc错误,至少对于问题中的内置分配而言,这听起来像是从引用到值的转换是“值计算” that shall be sequenced before the assignment的一部分。
关于在原始帖子的先前版本中报告的a strange behavior of clang-在断言和打印时返回不同的结果:
constexpr int foo() {
int res = 0;
(res = 5) |= (res *= 2);
return res;
}
int main() {
std::cout << foo() << std::endl; // prints 5
assert(foo() == 5); // fails in clang 11.0 - constexpr foo() is 10
// fixed in clang 11.x - correct value is 5
}
这与a bug in clang有关。 assert的错误是错误的,并且是由于在编译期间不断求值期间,此表达式在clang中的求值顺序错误。值应为5。此错误is already fixed在Clang干线中。