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

What is the correct answer for cout << a++ << a;?

提问人:pravs 提问时间:5/28/2012 最后编辑:R Sahupravs 更新时间:10/24/2017 访问量:12945

问:

在最近的一次采访中,有一个客观类型的问题。

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

答案:

A. 10
B. 01
C. 未定义的行为

我回答了选项 b,即输出将是“01”。

但后来令我惊讶的是,面试官告诉我,正确答案是选项c:未定义。

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

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

但根据我对语句的理解,会被调用两次,第一次是 with 和 后来。cout << a++ << aostream.operator<<()ostream.operator<<(a++)ostream.operator<<(a)

我还在 VS2010 编译器上检查了结果,其输出也是“01”。

C C++-常见问题

评论

30赞 Brady 5/28/2012
你要求解释吗?我经常面试潜在的候选人,并且对收到问题非常感兴趣,这表明我感兴趣。
3赞 James Kanze 5/28/2012
@jrok 这是未定义的行为。实施所做的任何事情(包括以您的名义向您的老板发送侮辱性电子邮件)都是合规的。
2赞 CB Bailey 5/28/2012
这个问题需要一个不提及序列点的 C++11(当前版本的 C++)答案。不幸的是,我对 C++11 中序列点的替换不够了解。
3赞 leftaroundabout 5/28/2012
如果它不是未定义的,它肯定不可能是,它将是 或 。(将始终计算为递增之前的值)。即使它不是未定义的,它仍然会非常令人困惑。100100c++c
2赞 tchrist 5/29/2012
你知道,当我读到标题“cout << c++ << c”时,我一时想到它是关于 C 和 C++ 语言之间关系的陈述,还有另一个叫做“cout”的陈述。你知道,就像有人说他们认为“cout”远不如C++,而C++远不如C——可能是因为传递性,“cout”非常非常不如C:)。

答:

68赞 Alok Save 5/28/2012 #1

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

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

代码语句:

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

评估如下:

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

该标准没有定义函数参数的计算顺序。
所以要么:

  • std::operator<<(std::cout, a++)首先评估或
  • a首先评估或
  • 它可以是任何实现定义的顺序。

根据标准,此订单未指定[参考文献 1]。

[参考文献 1]C++03 5.2.2 函数调用
第 8 段

参数的计算顺序未指定。参数表达式计算的所有副作用都会在函数输入之前生效。后缀表达式和参数表达式列表的计算顺序未指定。

此外,在函数的参数计算之间没有序列点,但只有在计算所有参数之后才存在序列点[参考文献 2]。

[参考文献 2]C++03 1.9 程序执行 [intro.execution]第 17 段:

调用函数时(无论函数是否内联),在函数体中执行任何表达式或语句之前,在计算所有函数参数(如果有)之后都会有一个序列点。

请注意,这里的值被多次访问,没有中间的序列点,关于这一点,标准说:c

[参考文献 3]C++03 5 表达式 [expr]第 4 段:

....
在上一个和下一个序列点之间,标量对象的存储值最多应通过表达式的计算进行一次修改。此外,应仅访问先前的值以确定要存储的值。对于完整子表达式的每个允许排序,应满足本款的要求 表达;否则,行为是未定义的

代码在不干预序列点的情况下多次修改,并且不会访问它以确定存储对象的值。这显然违反了上述条款,因此标准规定的结果是未定义行为[参考文献 3]。c

评论

0赞 jrok 5/28/2012
我的意思是,只修改一次,所以程序可以合法地打印 01 或 10,但不能做一些奇怪的事情。我的理解正确吗?c
1赞 James Kanze 5/28/2012
从技术上讲,该行为是未定义的,因为对对象进行了修改,并在没有干预序列点的情况下在其他地方访问它。Undefined 不是未指定;它给实施留下了更多的余地。
1赞 James Kanze 5/28/2012
@Als是的。我没有看到你的编辑(尽管我对 jrok 的声明做出了反应,即该程序不能做一些奇怪的事情---它可以)。就目前而言,您编辑的版本很好,但在我看来,关键字是部分排序;序列点仅引入部分排序。
4赞 Rafał Dowgird 5/28/2012
新的C++0x标准基本相同,但章节不同,措辞不同:)引用:(1.9 程序执行 [intro.execution],第 15 段):“如果标量对象上的副作用相对于同一标量对象的另一个副作用或使用同一标量对象的值计算是未排序的,则行为是未定义的。
2赞 Christopher Smith 9/21/2013
我相信这个答案有一个错误。“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”的计算之间有一个序列点(重载运算符被视为函数调用,因此当函数调用返回时有一个序列点)。因此,指定了行为和执行顺序。
148赞 Maxim Egorushkin 5/28/2012 #2

你可以想到:

cout << a++ << a;

奥斯特

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

C++ 保证先前评估的所有副作用都将在序列点执行。函数参数求值之间没有序列点,这意味着可以在参数之前或之后求值参数。所以上面的结果是不确定的。astd::operator<<(std::cout, a++)


C++17 更新

在 C++17 中,规则已更新。特别:

在移位运算符表达式 和 中,的每个值计算和副作用都先于每个值计算和副作用进行排序。E1<<E2E1>>E2E1E2

这意味着它需要代码来生成 result ,它输出 .b01

有关详细信息P0145R3请参阅优化惯用C++的表达式计算顺序

评论

0赞 pravs 5/28/2012
@Maxim:感谢您的解释。对于您痛苦的电话,这将是未定义的行为。但是现在,我还有一个问题(可能更沉默,我错过了一些基本的东西,并且大声思考)您如何推断将调用 std::operator<<() 的全局版本而不是 ostream::operator<<() 成员版本。在调试时,我登陆的是 ostream::operator<<() 调用的成员版本而不是全局版本,这就是最初我认为答案是 01 的原因
0赞 James Kanze 5/28/2012
@Maxim 并不是说它有所不同,而是由于 has type ,这里是成员函数。cintoperator<<
2赞 Maxim Egorushkin 5/28/2012
@pravs:是成员函数还是独立函数不影响序列点。operator<<
11赞 Rafał Dowgird 5/28/2012
C++ 标准中不再使用“序列点”。它不精确,已被“排序之前/排序后排序”关系所取代。
2赞 Deduplicator 5/31/2014
So the result of the above is undefined.您的解释仅适用于未指定,不适用于未定义。JamesKanze在他的回答中解释了为什么它更的不确定
20赞 James Kanze 5/28/2012 #3

序列点仅定义部分排序。就您而言,您有 (一旦完成过载解决):

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

在 和第一次调用之间有一个序列点,并且在 第二个和第二个调用,但有 在 和 之间没有序列点;唯一的订购 约束条件需要充分评估(包括副作用) 在第一次调用之前,第二次调用 完全 在第二次调用 之前进行评估。(还有 causual 排序约束:第二次调用 cannot 在第一个之前,因为它需要第一个的结果作为 参数。§5/4 (C++03) 规定:a++std::ostream::operator<<astd::ostream::operator<<a++aa++operator<<aoperator<<operator<<

除非另有说明,否则 计算单个运算符的操作数和子表达式 单个表情,以及副作用发生的顺序, 未指定。在上一个序列点和下一个序列点之间有一个标量 对象的存储值最多应由 表达式的计算。此外,先前的值应为 仅访问以确定要存储的值。要求 对于每个允许的顺序,应满足本款要求 完整表达式的子表达式;否则,该行为是 定义。

表达式的允许顺序之一是 , , first 调用 ,第二次调用 ;这将修改 存储值 (),并访问它,而不是确定 新值(第二个),行为未定义。a++aoperator<<operator<<aa++a

评论

0赞 Christopher Smith 9/21/2013
从您对标准的引用中得到一个收获。IIRC 在处理重载运算符时包含一个异常,该异常将运算符视为一个函数,因此在对 std::ostream::operator<<(int) 的第一次和第二次调用之间创建一个序列点。如果我错了,请纠正我。
0赞 James Kanze 9/21/2013
@ChristopherSmith 重载运算符的行为类似于函数调用。如果是定义了 user 的用户类型,而不是 ,则结果将未指定,但不会有未定义的行为。c++int
1赞 James Kanze 5/2/2014
@ChristopherSmith 你在哪里看到两者之间的序列点?调用函数和返回函数时都有一个序列点,但在两者的计算之间不需要函数调用。cfoo(foo(bar(c)), c)c
1赞 James Kanze 12/31/2014
@ChristopherSmith 如果是 UDT,则重载运算符将是函数调用,并将引入序列点,因此行为不会是未定义的。但是仍然没有指定子表达式是在之前还是之后计算的,因此不会指定您是否获得增量版本(理论上,不必每次都相同)。ccc++
1赞 James Kanze 1/5/2015
@ChristopherSmith 序列点之前的所有内容都将在序列点之后的任何内容之前发生。但序列点只定义了部分排序。例如,在所讨论的表达式中,子表达式和 之间没有序列点,因此两者可以以任何顺序出现。至于分号...它们只在完整表达式的范围内引起序列点。其他重要的序列点是函数调用:将看到递增的 、 和逗号运算符 ,以及 导致序列点。cc++f(c++)cf&&||?:
4赞 Paul Marrington 5/30/2012 #4

正确答案是质疑问题。这种说法是不可接受的,因为读者看不到明确的答案。另一种看待它的方法是,我们引入了副作用 (c++),使语句更难解释。简洁的代码很棒,只要它的含义很清楚。

评论

4赞 P.P 8/19/2013
该问题可能表现出糟糕的编程实践(甚至是无效的 C++)。但是答案应该回答问题,指出问题出在哪里以及为什么错了。对问题的评论不是答案,即使它们完全有效。充其量,这可能是一个评论,而不是一个答案。