使用C++11,写f(x++),g(x++)是否属于未定义行为?

42

我在阅读这个问题:

未定义行为和顺序点

特别是C++11的答案,我理解“评估的顺序”概念。但是,当我写下f(x++), g(x++);时,是否有足够的顺序?

也就是说,我保证f()获取x的原始值,g()获取一个增加后的x吗?

挑剔者注意事项:

  • 假设operator++()具有定义的行为(即使我们已经重载了它),f()g()都具有定义的行为,不会抛出异常等-该问题与此无关。
  • 假设没有重载operator,()

10
内置的,运算符是可行的,如果,运算符是用户定义的,则不可行。参见:http://en.cppreference.com/w/cpp/language/operator_other#Built-in_comma_operator(这在C++17中有所改变)。 - Richard Critten
19
如果你把逗号改成分号,就能保证了。 :-) - Bo Persson
8
如果我在这个表达式前面加上abort();,但那并没有太多帮助,对吗? - einpoklum
3
在N4659标准中,operator,( f(x++), g(x++) );不是未定义行为,如果假设x==0,则会先调用f(0)再调用g(1),或者先调用g(0)再调用f(1);执行完后,x的值将变成2。之前曾经提出过函数调用应该采用从左到右的严格顺序计算,但据我所知目前还没有实现。 - M.M
1
@snb:恕我直言,这并不是重复问题;我询问的是带有逗号表达式的行为。 - einpoklum
显示剩余8条评论
3个回答

49
不,行为已经定义了。引用 C++11(n3337)[expr.comma/1]:“逗号分隔的一对表达式从左到右求值;左表达式是一个弃值表达式(Clause [expr])。与左表达式相关的每个值计算和副作用都会在与右表达式相关的每个值计算和副作用之前被排序。” 我认为“every”表示“所有”1。第二个 x++ 的评估不能在调用序列到 f 完成并返回之前发生。2


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


1
@Mehrdad - 对象的生命周期是完全不同的一个事情。对象在逗号运算符中被评估之前不会被创建,但它不会死亡直到整个表达式结束。这就是临时对象应该表现的方式。 - StoryTeller - Unslander Monica
1
@Mehrdad - 你不能真的质疑C++标准本身在这些问题上的准确性。 - StoryTeller - Unslander Monica
@StoryTeller:我知道它的行为方式,也知道为什么会这样,我只是觉得它似乎直接违背了这里的引用。引用字面上说“与左表达式相关的每个值计算和副作用”,而你明确说你“将‘每个’理解为‘每个’”,而析构函数调用无疑是与左表达式“相关”的副作用...所以最好的情况是你需要在引用中添加一些上下文,最坏的情况要么是你要么是标准错误/自相矛盾。 - user541686
2
@StoryTeller:哦,抱歉,好的,谢谢,这绝对是违反直觉的:“计算临时对象的值和副作用仅与完整表达式相关,而不与任何特定子表达式相关。” 这绝对不是我预期的,因为从逻辑上讲,表达式的析构函数调用*与其相关。我建议在您的答案中添加析构函数调用被排除在此之外的内容,因为对于没有阅读标准的人来说,它会产生不同的印象... - user541686
1
@Mehrdad - 当然可以。我添加了另一个脚注。 - StoryTeller - Unslander Monica
显示剩余6条评论

23

不,这不是未定义行为。

根据此评估顺序和排序参考,逗号左侧在右侧之前完全评估 (见规则 9):

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

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

请注意,这仅适用于内置逗号运算符。


12
而且,需要强调的是,这不仅适用于C++11,自古以来就一直如此。 - Pete Becker
1
但规则已经改变,序列点不再被使用。我怀疑意图并没有改变,但基于序列点的解释在内部是不完整的,因此是无法得出结论的。 - Yttrill

12

这要看情况。

首先,假设x++本身不会引起未定义行为。请考虑有符号数溢出、增量一个超过末尾指针或者后缀递增运算符可能是用户定义的情况。
此外,假设使用参数调用f()g()并销毁这些临时变量不会引起未定义行为。
这是相当多的假设,但如果它们被破坏了,答案就很显然了。

现在,如果逗号是内置逗号运算符、花括号初始化列表中的逗号或者成员初始化列表中的逗号,则左侧和右侧都会被顺序执行(您知道哪个在前面或后面),因此不会产生干扰,使行为具有明确定义。

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++));

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接