【问题标题】:Chained compound assignments with C++17 sequencing are still undefined behaviour?使用 C++17 排序的链式复合赋值仍然是未定义的行为?
【发布时间】:2021-01-19 13:02:46
【问题描述】:

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

以下程序在 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(强调我的):

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

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

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

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

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

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

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

  1. 右侧a 被评估为5。
  2. 左侧被评估为a 的引用,a += 1 肯定是明确定义的。
  3. 评估分配,将a 的当前值加 5,使其变为 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 可能是错误的。

【问题讨论】:

  • 读取程序集无助于调试编译时间计算,因为编译器会输出这些结果。
  • 为什么要坚持在同一行使用*=+=?把它分成两行。即使它被正确排序,它仍然对人类来说是不可读的(或者如果你想要一个更温和的陈述,则会增加认知负担)
  • @Jeffrey 我明确表示我不关心此类代码的可读性,我想知道它为什么不起作用。两行折叠表达式怎么写?
  • 好的,但现在问题相当复杂。如果您确定可以将问题简化为(a += 1) += a;,则删除其他所有内容。否则,提出一个新问题来询问这个问题,如果你愿意,可以链接到这个问题作为动机。 (请注意,这个问题已经在要求两件事,一个解释和一个修复。这可能是区分它们的好方法)。
  • 是的,现在清楚多了。是的,如果这不是 UB,那么单独发布原始问题是个好主意。另外,我稍微调整了一下标签。

标签: c++ c++17 language-lawyer sequence-points


【解决方案1】:

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

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在原帖的上一版本中报告-在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
}

这与a bug in clang 有关。断言失败是错误的,并且是由于在编译时不断评估期间这个表达式在 clang 中的错误评估顺序。该值应该是 5。这个 bug is already fixed 在 clang trunk 中。

【讨论】:

  • 我相信我犯了一个错误,我的第二个订单可能被允许 - 分配可以交错左右排序。在那种情况下,是的 gcc 的价值计算似乎很粗略,但我的解释可能是正确的。如果值计算导致引用,则可能正是由于交错而未定义。
  • 看起来 gcc 由于括号而将 (a += 1) += a;a += (a += 1) 视为等效项,并因此推迟引用转换。这种行为对于所有版本的 gcc 都是一致的,因为 pre-C++11
猜你喜欢
  • 2014-05-01
  • 2015-07-30
  • 1970-01-01
  • 2019-03-16
  • 1970-01-01
  • 2023-03-02
  • 2017-12-23
  • 2021-04-21
  • 2019-12-17
相关资源
最近更新 更多