使用 C++17 排序的链式复合赋值仍然是未定义的行为?

Chained compound assignments with C++17 sequencing are still undefined behaviour?

本来我举了一个比较复杂的例子,这个是@n提出来的。 'pronouns'米。在现已删除的答案中。但是问题变得太长了,有兴趣的可以看看编辑历史

以下程序在 C++17 中是否具有明确定义的行为?

int main()
{
    int a=5;
    (a += 1) += a;
    return a;
}

我相信这个表达式的定义和计算是这样的:

  1. 右侧 a 计算为 5。
  2. 右侧没有副作用。
  3. 左侧被评估为对 a 的引用,a += 1 肯定是明确定义的。
  4. 左侧副作用被执行,使得a==6.
  5. 评估赋值,将 5 添加到 a 的当前值,使其成为 11。

标准的相关章节:

[intro.execution]/8:

An expression X is said to be sequenced before an expression Y if every value computation and every side effect associated with the expression X is sequenced before every value computation and every side effect associated with the expression Y.

[expr.ass]/1(强调我的):

The assignment operator (=) and the compound assignment operators all group right-to-left. All require a modifiable lvalue as their left operand; their result is an lvalue referring to the left operand. The result in all cases is a bit-field if the left operand is a bit-field. In all cases, the assignment is sequenced after the value computation of the right and left operands, and before the value computation of the assignment expression. The right operand is sequenced before the left operand. With respect to an indeterminately-sequenced function call, the operation of a compound assignment is a single evaluation.

该措辞最初来自已接受的论文P0145R3

现在,我觉得在第二部分中有些歧义,甚至是矛盾。

The right operand is sequenced before the left operand.

连同sequenced before的定义强烈暗示了副作用的排序,然而前一句:

In all cases, the assignment is sequenced after the value computation of the right and left operands, and before the value computation of the assignment expression

仅在值计算后显式排序赋值,而不是它们的副作用。因此允许这种行为:

  1. 右侧 a 计算为 5。
  2. 左侧被评估为 a 的引用,a += 1 肯定是明确定义的。
  3. 评估赋值,将 5 添加到 a 的当前值,使其成为 10。
  4. 左侧的副作用被执行,使得 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 可能是错误的。

为了更好地了解实际执行的内容,让我们尝试用我们自己的类型模仿相同的内容并添加一些打印输出:

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

这与问题中整数的结果相同。 即使它不是完全相同的故事: built-in 赋值有自己的顺序规则,与函数调用的重载运算符相反,相似之处仍然很有趣。

似乎 gcc 将右侧作为参考并在调用 += 时将其转换为一个值,clang 另一方面先把右边变成一个值。

下一步是向我们的 Number class 添加一个复制构造函数,以准确遵循引用转换为值的时间。 Doing that 结果 调用复制构造函数作为第一个操作,通过 clang 和 gcc, 两者的结果相同:11.

似乎 gcc 延迟了对值转换的引用(在 built-in 赋值以及没有用户的用户定义类型中defined copy constructor). 它是否与 C++17 定义的顺序一致?在我看来,这似乎是一个 gcc 错误,至少对于问题中的 built-in 赋值而言,因为听起来从引用到值的转换是“值计算”的一部分 that shall be sequenced before the assignment.


至于a strange behavior of clang在原post之前版本中的报道-在assert和打印时返回不同的结果:

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
}

这与 clang t运行k 中的 a bug in clang. The failure of the assert is wrong and is due to wrong evaluation order of this expression in clang, during constant evaluation in compile time. The value should be 5. This bug is already fixed 有关。