首页 > 解决方案 > 使用 C++17 排序的链式复合赋值仍然是未定义的行为?

问题描述

最初,我提出了一个更复杂的例子,这个例子是由@n 提出的。'代词' m。在一个现已删除的答案中。但是问题变得太长了,如果您有兴趣,请查看编辑历史。

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

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

我相信这个表达式是明确定义和评估的:

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

标准的相关章节:

[intro.execution]/8

如果与表达式 X 关联的每个值计算和每个副作用都在每个值计算和与表达式 Y 关联的每个副作用之前排序,则表示表达式 X 在表达式 Y 之前排序。

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

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

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

现在,我觉得这第二部分有些模棱两可,甚至矛盾。

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

前序的定义一起强烈暗示了副作用的排序,但前一句:

在所有情况下,赋值顺序在左右操作数的值计算之后,赋值表达式的值计算之前

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

  1. 右侧a评估为 5。
  2. 左侧被评估为 , 的引用a肯定a += 1是明确定义的。
  3. 评估分配,将 的当前值加 5 a,使其变为 10。
  4. 左侧的副作用被执行,a==11甚至可能即使6旧值被用于副作用。

但是这种排序显然违反了sequenced before的定义,因为左操作数的副作用发生在右操作数的值计算之后。因此,左操作数没有排在使上述句子变紫的右操作数之后。 不,我做错了。这是允许的行为,对吗?即分配可以交错左右评估。或者可以在两次全面评估后完成。

运行代码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 可能是错误的。

标签: c++c++17language-lawyersequence-points

解决方案


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

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;

我们使用gccclang得到不同的结果(并且没有任何警告!)。

海湾合作委员会:

+=(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 类添加一个复制构造函数,以便在引用转换为值时准确遵循。通过clang和 gcc调用复制构造函数作为第一个操作,结果是相同的: 11

似乎gcc延迟了对值转换的引用(在内置赋值以及没有用户定义的复制构造函数的用户定义类型中)。它与 C++17 定义的序列一致吗?对我来说,这似乎是一个 gcc 错误,至少对于问题中的内置赋值,因为听起来从引用到值的转换是“值计算”的一部分,应该在赋值之前进行排序


至于在原始帖子的先前版本中报告的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
}

这与clang 中的一个错误有关。断言失败是错误的,并且是由于在编译时不断评估期间这个表达式在 clang 中的错误评估顺序。该值应为 5。此错误在 clang 主干中修复。


推荐阅读