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


98

最近在一次采访中,出现了以下客观类型问题。

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

答案:

一个。10
羽 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
您是否要求解释?我经常面试潜在的候选人,并且对接收问题非常感兴趣,这显示出了兴趣。
布雷迪

3
@jrok这是未定义的行为。该实施所做的一切(包括以您的名义向老板发送侮辱性电子邮件)都是符合要求的。
詹姆斯·坎泽

2
对于没有提及序列点的C ++ 11(C ++的当前版本)答案,这个问题引起了广泛的关注。不幸的是,我对C ++ 11中序列点的替换知识不足。
CB Bailey

3
如果不是不确定它绝对不可能是10,这将是两种0100。(c++将始终评估为价值c之前递增)。即使它不是未定义的,也仍然会令人困惑。
左右左转

2
没错,当我读到标题“ cout << c ++ << c”时,我立即将其视为有关C语言和C ++语言以及其他一些名为“ cout”的关系的陈述。你知道,像有人说他们是怎么认为“COUT”远远落后于C ++,和C ++远远落后到C -可能通过传递,即“COUT”是非常,非常远不如C. :)
tchrist

Answers:


145

您可以想到:

cout << a++ << a;

如:

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

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


C ++ 17更新

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

在移位运算符表达式E1<<E2and中E1>>E2,的每个值计算和副作用E1在的每个值计算和副作用之前排序E2

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

有关更多详细信息,请参见P0145R3惯用C ++的优化表达式评估顺序


@Maxim:感谢您的解释。使用您解释过的电话,这将是不确定的行为。但是现在,我还有一个问题(可能是一个问题,我错过了一些基本知识并且大声思考)您如何推断出将调用全局版本的std :: operator <<()而不是ostream :: operator < <()成员版本。在调试时,我使用的是ostream :: operator <<()调用的成员版本,而不是全局版本,因此我最初认为答案是01
pravs

@Maxim并非有所不同,但由于c具有type int,因此operator<<这里是成员函数。
詹姆斯·坎泽

2
@pravs:operator<<是成员函数还是独立函数不会影响序列点。
Maxim Egorushkin 2012年

11
C ++标准中不再使用“序列点”。它是不精确的,已被“先后顺序/后先顺序”关系取代。
2012年

2
So the result of the above is undefined.您的解释仅适用于未指定的情况,不适用于未定义的情况。詹姆斯·坎泽(JamesKanze)解释了在他的回答中如何定义 的更令人讨厌。
Deduplicator 2014年

68

从技术上讲,总体来说这是Undefined Behavior

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

代码语句:

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

评估为:

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

该标准未定义函数自变量的求值顺序。
所以:

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

根据标准,此订单未指定[Ref 1]

[Ref 1] C ++ 03 5.2.2函数调用
第8段

对论证的评估顺序是不确定的。输入函数之前,参数表达式求值的所有副作用都将生效。后缀表达式和参数表达式列表的求值顺序未指定。

此外,在对函数的参数进行评估之间没有序列点,但是仅在对所有参数进行评估之后才存在序列点[Ref 2]

[Ref 2] C ++ 03 1.9程序执行[intro.execution]:
第17段:

调用函数时(无论函数是否为内联),所有函数参数(如果有)的求值后都有一个序列点,该序列点发生在函数体内任何表达式或语句的执行之前。

请注意,在这里,在c没有中间顺序点的情况下,多次访问的值,对此标准表示:

[Ref 3] C ++ 03 5表达式[expr]:
第4段:

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

该代码c在不插入序列点的情况下进行了多次修改,并且无法访问该代码以确定存储对象的值。这显然违反了上述条款,因此标准要求的结果是未定义行为[Ref 3]


1
从技术上讲,该行为是不确定的,因为对对象进行了修改,并且可以在没有中间序列点的情况下在其他位置访问它。未定义不是未指定;它给实现留出了更多的余地。
詹姆斯·坎泽

1
@Als是的。我没有看到您的编辑(尽管我对jrok的声明做出了反应,该声明该程序无法执行某些奇怪的操作-它可以执行)。您所编辑的版本目前为止还不错,但是在我看来,关键词是部分排序;序列点仅引入部分排序。
詹姆斯·坎泽

1
@Als感谢详尽的描述,真的非常有帮助!
pravs

4
新的C ++ 0x标准在本质上是相同的,但是在不同的部分和使用不同的措词:) Quote:(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”的求值之间确实有一个序列点(重载的运算符被认为是函数调用,因此函数调用返回时会有一个序列点。因此,该行为和执行顺序规定。
克里斯托弗·史密斯

20

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

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

a++和的第一个调用 之间std::ostream::operator<<有一个序列点,在第二个a和第二个调用之间有一个序列点std::ostream::operator<<,但是a++和之间没有序列点a。唯一的排序约束是a++在第一次调用之前要充分评估(包括副作用)operator<<,而在第二a次调用之前要充分评估第二个约束operator<<。(也存在因果排序约束:对的第二次调用operator<<不能在第一个调用之前,因为它需要将第一个的结果作为参数。)§5/ 4(C ++ 03)指出:

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

你的一个表达的允许排序的是a++a首先调用operator<<,第二次调用operator<<; 这会修改aa++)的存储值,并且除了确定新值(第二个a)以外,对其进行访问,该行为未定义。


从您对标准的报价中可以得出一个结论。IIRC的“除非另有说明”,在处理重载运算符时包含一个异常,该异常将运算符视为函数,因此在对std :: ostream :: operator <<(int )。如果我错了,请纠正我。
克里斯托弗·史密斯

@ChristopherSmith重载的运算符的行为类似于函数调用。如果c是具有用户定义++而不是的用户类型int,则结果将不确定,但不会有未定义的行为。
James Kanze 2013年

1
@ChristopherSmith您在哪里看到两个cin 之间的序列点foo(foo(bar(c)), c)?在调用函数以及返回函数时都有一个序列点,但是在两个求值之间不需要调用函数c
詹姆斯·坎泽

1
@ChristopherSmith如果c是UDT,则重载的运算符是函数调用,并将引入一个序列点,因此该行为不会是不确定的。但是仍然不确定该子表达式c是在之前还是之后进行求值的c++,因此将不指定是否获得递增版本(理论上,每次不必相同)。
James Kanze 2014年

1
@ChristopherSmith序列点之前的所有内容都将在序列点之后的所有内容之前发生。但是序列点仅定义部分排序。例如,在所讨论的表达式中,子表达式c和之间没有序列点c++,因此两者可以以任何顺序出现。至于分号...只要它们是完整表达式,它们只会引起序列点。其他重要的序列点的函数调用:f(c++)会看到增加cf,而逗号操作符,&&||并且?:还会引起序列点。
James Kanze

4

正确答案是对问题的质疑。该声明是不可接受的,因为读者看不到明确的答案。另一种看待它的方式是我们引入了副作用(c ++),这些副作用使该语句更难以解释。简明扼要的代码很棒,只要它的意思很清楚即可。


4
这个问题可能表现出不良的编程习惯(甚至是无效的C ++)。但是应该回答一个问题,指出什么是错误的以及为什么它是错误的。即使问题完全正确,对问题的评论也不是答案。充其量,这可以是评论,而不是答案。
2013年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.