cout << a++ << a; 的正确答案是什么?

100

最近在一次面试中,有一个如下的客观类型问题。

int a = 0;
cout << a++ << a;

答案:

a. 10
b. 01
c. 未定义行为

我的回答是选项b,即输出应该是"01"。

但后来我被面试官告知正确答案是选项c:未定义行为。

现在,我知道C++中的序列点概念。以下语句的行为是未定义的:

int i = 0;
i += i++ + i++;

根据我的理解,对于语句 cout << a++ << a,会调用两次ostream.operator<<(),首先是ostream.operator<<(a++),然后是ostream.operator<<(a)

我在VS2010编译器上也进行了验证,输出结果也是'01'。


30
你有要求解释吗?我经常面试潜在候选人,很感兴趣听到提问,这表明对工作的兴趣。 - Brady
3
@jrok 这是未定义的行为。实现可能会做任何事情(包括以您的名义发送侮辱性电子邮件给您的老板),这些行为都被认为是符合标准的。 - James Kanze
2
这个问题迫切需要一份使用C++11(当前版本的C++)回答,而不涉及序列点。不幸的是,我对C++11中用于替代序列点的内容不够了解。 - CB Bailey
3
如果它不是未定义的,那么它肯定不能是 10,而是可能是 0100。(c++ 运算符会在自增之前使用变量 c 的值进行运算) 即使它不是未定义的,它仍然可能非常令人困惑。 - leftaroundabout
2
你知道吗,当我看到标题“cout << c++ << c”时,我瞬间把它想象成了关于C和C++语言之间以及另一个名为“cout”的东西之间关系的陈述。就像有人说他们认为“cout”远不如C++,而C++也远不如C - 可能通过传递性,“cout”非常、非常地不如C。 :) - tchrist
显示剩余8条评论
4个回答

148
您可以考虑:
cout << a++ << a;

作为:

std::operator<<(std::operator<<(std::cout, a++), a);

C++保证在序列点处执行先前评估的所有副作用。在函数参数评估之间没有序列点,这意味着参数a可以在参数std::operator<<(std::cout, a++)之前或之后被评估。因此,上述结果是未定义的。

C++17更新

C++17更新了规则,特别是:

在移位运算符表达式E1<<E2E1>>E2中,E1的每个值计算和副作用在E2的每个值计算和副作用之前被序列化。

这意味着它要求代码产生结果b,输出01

有关更多详细信息,请参见P0145R3 Refining Expression Evaluation Order for Idiomatic C ++


@Maxim:感谢您的解释。使用您所解释的调用将是未定义的行为。但现在,我有一个更多的问题(可能是愚蠢的问题,我可能错过了一些基本的东西并且正在思考)。您是如何推断出将调用std::operator<<()的全局版本而不是ostream::operator<<()成员版本的呢?在调试时,我陷入了ostream::operator<<()成员版本的调用中,而不是全局版本,这就是最初我认为答案将是01的原因。 - pravs
@Maxim 不是说这会有什么不同,但由于 c 的类型为 int,所以这里的 operator<< 是成员函数。 - James Kanze
2
@pravs:无论operator<<是成员函数还是自由函数,都不会影响序列点。 - Maxim Egorushkin
好的,谢谢James/Maxim。我告诉你我缺少了一些非常基础的东西。我对这个成员函数声明感到困惑:_Myt& __CLR_OR_THIS_CALL operator<<(int _Val)我在思考中错过了第一个参数是成员函数的“this”,所以对函数调用有点困惑。:) 干杯! - pravs
11
“序列点”在 C++ 标准中已经不再使用。它的描述不够准确,已被“先于/后于顺序”关系所取代。 - Rafał Dowgird
2
因此,上述结果未定义。您的解释仅适用于“未指定”,而不适用于“未定义”。JamesKanze在他的答案中解释了更加严重的“未定义”问题。请参考链接:https://dev59.com/hGgv5IYBdhLWcg3wMN60#10783921。 - Deduplicator

68

从技术上讲,总体来说这就是未定义行为

但是,这个答案有两个重要方面。

代码语句:

std::cout << a++ << a;

被评估为:

std::operator<<(std::operator<<(std::cout, a++), a);
标准不定义函数参数的评估顺序,可能是 std::operator<<(std::cout, a++) 先被评估或 a 先被评估或其他实现定义的顺序。这个顺序在标准中被称为未指定。在对一个函数的参数进行评估时不存在序列点,但在所有参数被评估后才存在序列点。如果对存储的标量对象在前后序列点之间进行表达式的求值,在每个完整表达式的子表达式允许的计算顺序下,该对象的存储值最多只能被修改一次,并且先前的值只能被访问以确定要存储的值。否则,行为是未定义的。因此,代码修改了超过一次存储对象 c 的值并且没有访问它来确定存储对象的值,这违反了上述规定,标准强制规定的结果是未定义行为。

1
从技术上讲,这种行为是未定义的,因为对象被修改,并且在没有中间序列点的情况下在其他地方访问它。 未定义不是未指定; 它使实现具有更大的灵活性。 - James Kanze
@JamesKanze:我不确定,但似乎你在我编辑之前看到了答案。现在我已经进行了编辑,并使用标准用语更加具体化,请随时指出我是否漏掉了什么。实际上,你的评论总是非常精准,毫无疑问,你的赞同对我来说非常重要。 - Alok Save
1
@Als 是的。我没有看到您的编辑(尽管我是在回应jrok的说法,即程序无法执行某些奇怪的操作——实际上它可以)。您编辑后的版本已经很好了,但在我看来,关键词是“偏序”;序列点只引入了偏序。 - James Kanze
4
新的C++0x标准基本上表达相同的意思,但是用了不同的章节和措辞。引用:(1.9 程序执行 [intro.execution],第15段):“如果对标量对象的副作用在与该标量对象的另一个副作用或使用相同标量对象的值计算在时间上未确定,则行为未定义。” - Rafał Dowgird
2
我认为这个答案中有一个错误。"std::cout<<c++<<c;" 不能被翻译成 "std::operator<<(std::operator<<(std::cout, c++), c)",因为 std::operator<<(std::ostream&, int) 并不存在。相反,它应该被翻译成 "std::cout.operator<<(c++).operator(c);",这样就确实在 "c++" 和 "c" 的评估之间有一个序列点(重载运算符被视为函数调用,因此当函数调用返回时就会有一个序列点)。因此行为和执行顺序是被指定的。 - Christopher Smith
显示剩余2条评论

20

序列点只定义了一个部分排序。在您的情况下,您有(一旦重载解析完成):

std::cout.operator<<( a++ ).operator<<( a );
a++和第一个 std::ostream::operator<< 调用之间存在一个序列点,同样在第二个 a 和第二个 std::ostream::operator<< 调用之间也存在一个序列点,但是在 a++a 之间没有序列点。唯一的顺序约束是必须在第一次调用 operator<< 之前完全评估 a++(包括副作用),并且必须在第二次调用 operator<< 之前完全评估第二个 a。 §5/4(C++03)规定:

除非另有说明,否则单个运算符的操作数和单个表达式的子表达式的求值顺序以及副作用发生的顺序均未指定。在上一个序列点和下一个序列点之间,标量对象通过表达式的评估最多只能修改其存储的值一次。此外,只能访问先前的值以确定要存储的值。对于完整表达式的子表达式的每个允许的排序,都必须满足本段的要求;否则行为是未定义的。

您的表达式的一种允许的排序是 a++a,第一个调用 operator<<,第二个调用 operator<<;这修改了 a 的存储值(a++),并且访问它不仅是为了确定新值(第二个 a),此行为是未定义的。


你从标准中引用的一个要点。如果我没记错,“除非另有说明”,包括在处理重载运算符时的一个例外,它将运算符视为函数,因此在第一次和第二次调用std::ostream::operator<<(int)之间创建一个序列点。如果我错了,请纠正我。 - Christopher Smith
@ChristopherSmith 重载运算符的行为类似于函数调用。如果c是一个用户类型,具有用户定义的++,而不是int,则结果将是未指定的,但不会有未定义的行为。 - James Kanze
1
@ChristopherSmith 在 foo(foo(bar(c)), c) 中的两个 c 之间你看到了一个序列点吗?当函数被调用和返回时,会有一个序列点,但在评估这两个 c 之间不需要进行函数调用。 - James Kanze
1
如果c是一个UDT,那么重载运算符将成为函数调用,并引入一个序列点,因此行为不会是未定义的。但是,子表达式cc++之前还是之后被评估仍然是未指定的,因此是否获得增量版本是未指定的(理论上,每次都不必相同)。 - James Kanze
1
@ChristopherSmith 在序列点之前的所有操作都会在序列点之后的任何操作之前发生。但是,序列点仅定义了部分顺序。例如,在所讨论的表达式中,子表达式cc++之间没有序列点,因此两者可以以任何顺序出现。至于分号...它们只有在作为完整表达式时才会引起序列点。其他重要的序列点包括函数调用:f(c++)将在f中看到增加的c,逗号运算符、&&||?:也会引起序列点。 - James Kanze
显示剩余6条评论

4

正确的答案是质疑问题本身。这个陈述不可接受,因为读者无法看到明确的答案。另一种看待它的方式是我们引入了副作用(C ++),使得该语句更难以解释。简洁的代码很好,只要其含义清晰。


4
这个问题可能展示了不良的编程实践(甚至是无效的C++代码)。但回答应该“回答”问题,指出什么是错误的以及为什么错误。对问题的评论即使它们是完全有效的也不能算作答案。最好的情况是这只能是一个评论,而不是答案。 - P.P

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