具有C ++ 17排序的链接化合物分配仍然是不确定的行为吗?

问题描述

最初,我提供了一个更复杂的示例,该示例由@n提出。代词现已删除的答案中。但是问题变得太长了,如果您有兴趣,请参阅编辑历史记录。

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

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

我相信此表达式定义明确,并经过如下评估:

  1. 右侧a的值为5。
  2. 右侧没有副作用。
  3. 左侧评估为对a的引用,a += 1的定义明确。
  4. 执行左侧副作用,使a==6
  5. 对赋值进行评估,将a的当前值加5,使其变为11。

标准的相关部分:

[intro.execution]/8

如果满足以下条件,则可以说表达式X在表达式Y之前被排序。 与该值相关的每个值计算和每个副作用 表达式X在每次值计算和每次 与表达式Y相关的副作用。

[expr.ass]/1(重点是我):

赋值运算符(=)和复合赋值运算符全部 从右到左分组。所有人都需要一个修改的左值 操作数它们的结果是一个指向左操作数的左值。的 如果左操作数是一个位字段,则在所有情况下结果都是一个位字段。 在所有情况下,分配都在值计算之后进行排序 左右操作数的和,在计算值之前 赋值表达式。右操作数在 左操作数。对于不确定顺序的函数调用,复合赋值的操作是单个求值。

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

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

右操作数在左操作数之前排序。

连同之前排序的的定义强烈暗示了副作用的排序,而前一句话是

在所有情况下,分配都在值计算之后进行排序 左右操作数的和,在计算值之前 赋值表达式

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

  1. 右侧a的值为5。
  2. 左侧评估为对a的引用,a += 1的定义明确。
  3. 对赋值进行评估,将a的当前值加5,使其变为10。
  4. 执行左侧副作用,如果甚至将旧值用于副作用,也会产生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干线中。