【问题标题】:With C++11, is it undefined behavior to write f(x++), g(x++)?使用 C++11,写 f(x++)、g(x++) 是未定义的行为吗?
【发布时间】:2017-08-22 10:34:09
【问题描述】:

我正在阅读这个问题:

Undefined behavior and sequence points

特别是C++11 answer,我理解评估“排序”的概念。但是——我写的时候有没有足够的顺序:

f(x++), g(x++);?

也就是说,我是否保证f() 获得x 的原始值,而g() 获得一次递增的x

吹毛求疵者的注意事项:

  • 假设 operator++() 已定义行为(即使我们已覆盖它),f()g() 也已定义行为,不会引发异常等 - 这个问题与此无关。李>
  • 假设operator,() 没有过载。

【问题讨论】:

  • 是的,对于内置的, 运算符。如果 , 运算符是用户定义的,则否。见:en.cppreference.com/w/cpp/language/…(随着 C++17 的变化)
  • 如果您将逗号更改为分号,则可以保证。 :-)
  • @BoPersson:另外,如果我在表达式前面加上abort();,但这不是很有帮助,是吗?
  • 在 N4659 中,operator,( f(x++), g(x++) ); 不是 UB,并且(例如 x == 0 事先)要么先调用 f(0),然后再调用 g(1),或者先调用 g(0),然后再调用 f(1);然后离开x == 2。有人提出函数调用有严格的左右评估,但据我所知,这并没有发生
  • @snb:完全没有,请在标记为骗子之前仔细阅读问题和潜在的骗子。 (提示:另一个问题是在哪一年提出的?)

标签: c++ c++11 expression undefined-behavior


【解决方案1】:

不,行为已定义。引用 C++11 (n3337) [expr.comma/1]:

一对用逗号分隔的表达式从左到右计算; 左边的表达式是丢弃值表达式(子句 [expr])。 与左相关的每个值计算和副作用 表达式在每个值计算和副作用之前排序 与正确的表达相关联。

我将“每个”理解为“每个”1。在对f 的调用序列完成并且f 返回之前,不能对第二个x++ 进行评估。2


1 析构函数调用不与子表达式相关联,仅与完整表达式相关联。因此,您将看到在完整表达式末尾以与临时对象创建相反的顺序执行。
2 本段仅适用于用作运算符时的逗号。当逗号具有特殊含义时(例如在指定函数调用参数序列时),这不适用。

【讨论】:

  • 引用的第一句话可能会产生误导:它只能在逗号​​运算符的上下文中使用。在代码func( f(x++), g(x++) ); 中,我们当然有一对用逗号分隔的表达式,但此引用不适用于该代码
  • @Mehrdad - 对象生命周期完全不同。该对象在逗号运算符中的求值到来之前不会复活,但在整个表达式结束之前它不会死亡。这就是临时工的行为方式。
  • @Mehrdad - 你不能真正质疑 C++ 标准本身在这些问题上的准确性。
  • @StoryTeller: 哦抱歉,好的,谢谢,这绝对是违反直觉的:"销毁临时对象的值计算和副作用仅与完整表达式相关,与任何特定表达式无关子表达式。” 绝对不是我所期望的,因为从逻辑上讲,表达式的析构函数调用 is 与之相关联。我建议在您的答案中添加析构函数调用被排除在外,因为它对没有阅读标准的人提出了其他建议......
  • @Mehrdad - 当然可以。添加了另一个脚注
【解决方案2】:

不,这不是未定义的行为。

根据this evaluation order and sequencing reference,逗号的左侧在右侧之前被完全评估(参见rule 9):

9) 内置逗号运算符的第一个(左)参数的每个值计算和副作用,在第二个(右)参数的每个值计算和副作用之前排序。

这意味着像f(x++), g(x++) 这样的表达式不是未定义的。

请注意,这仅对内置逗号运算符有效。

【讨论】:

  • 而且,强调一下,这不是 C++11 独有的。自古以来就是如此。
  • 但是规则已经改变,序列点不再使用。我怀疑意图已经改变,但是基于序列点的解释在内部是不完整的,因此没有结论。
【解决方案3】:

视情况而定。

首先,让我们假设x++ 本身不会调用未定义的行为。想想有符号溢出、增加过去的指针,或者后缀增量运算符可能是用户定义的)。
此外,我们假设使用它们的参数调用 f()g() 并销毁临时对象不会调用未定义的行为。
这是相当多的假设,但如果它们被打破,答案就是微不足道的。

现在,如果逗号是内置的逗号操作符、花括号初始化列表中的逗号或 mem-initializer-list 中的逗号,则左侧和右侧在每个之前或之后排序其他(你知道哪个),所以不要干涉,使行为明确。

struct X {
    int f, g;
    explicit X(int x) : f(x++), g(x++) {}
};
// Demonstrate that the order depends on member-order, not initializer-order:
struct Y {
    int g, f;
    explicit Y(int x) : f(x++), g(x++) {}
};
int y[] = { f(x++), g(x++) };

否则,如果x++ 为后缀增量调用用户定义的运算符重载,则x++ 的两个实例的顺序不确定,因此行为未指定。

std::list<int> list{1,2,3,4,5,6,7};
auto x = begin(list);
using T = decltype(x);

void h(T, T);
h(f(x++), g(x++));
struct X {
    X(T, T) {}
}
X(f(x++), g(x++));

在最后一种情况下,由于x 的两个后缀增量未排序,因此您会得到完整的未定义行为。

int x = 0;

void h(int, int);
h(f(x++), g(x++));
struct X {
    X(int, int) {}
}
X(f(x++), g(x++));

【讨论】:

    猜你喜欢
    • 2011-06-22
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-04-14
    • 1970-01-01
    相关资源
    最近更新 更多