9

我是介绍 C++ 课程的助教。上周在一次测试中提出了以下问题:

以下程序的输出是什么:

int myFunc(int &x) {
   int temp = x * x * x;
   x += 1;
   return temp;
}

int main() {
   int x = 2;
   cout << myFunc(x) << endl << myFunc(x) << endl << myFunc(x) << endl;
}

对我和我所有的同事来说,答案显然是:

8
27
64

但现在有几个学生指出,当他们在某些环境中运行它时,他们实际上得到了相反的结果:

64
27
8

当我使用 gcc 在我的 linux 环境中运行它时,我得到了我所期望的。在我的 Windows 机器上使用 MinGW 我明白他们在说什么。它似乎首先评估对 myFunc 的最后一次调用,然后是第二次调用,然后是第一次调用,然后一旦获得所有结果,它就会以正常顺序输出它们,从第一个开始。但是因为电话是乱序的,所以数字是相反的。

在我看来是编译器优化,选择以相反的顺序评估函数调用,但我真的不知道为什么。我的问题是:我的假设是否正确?这就是在后台发生的事情吗?或者有什么完全不同的东西?另外,我真的不明白为什么向后评估函数然后向前评估输出会有好处。由于 ostream 的工作方式,输出必须向前,但似乎对函数的评估也应该向前。

谢谢你的帮助!

4

6 回答 6

15

C++ 标准没有定义完整表达式的子表达式的求值顺序,除了某些引入顺序的运算符(逗号运算符、三元运算符、短路逻辑运算符)以及构成函数/运算符的参数/操作数都在函数/运算符本身之前进行评估。

GCC 没有义务向您(或我)解释它为什么要像现在这样订购它们。这可能是性能优化,可能是因为编译器代码以这种方式缩短了几行并且更简单,可能是因为 mingw 编码员之一个人讨厌你,并希望确保如果你做出假设不是' t 由标准保证,您的代码会出错。欢迎来到开放标准的世界:-)

编辑以添加: litb 在以下关于(未)定义的行为提出了一点。该标准规定,如果您在一个表达式中多次修改一个变量,并且如果该表达式存在一个有效的求值顺序,使得该变量被多次修改而中间没有序列点,则该表达式具有未定义的行为。这不适用于这里,因为变量在函数调用中被修改,并且在任何函数调用的开头都有一个序列点(即使编译器内联它)。但是,如果您手动内联代码:

std::cout << pow(x++,3) << endl << pow(x++,3) << endl << pow(x++,3) << endl;

那将是未定义的行为。在这段代码中,编译器对所有三个“x++”子表达式求值是有效的,然后是对 pow 的三个调用,然后是对 的各种调用operator<<。因为这个顺序是有效的,并且没有分隔 x 的修改的序列点,所以结果是完全不确定的。在您的代码片段中,仅未指定执行顺序。

于 2009-10-01T15:53:11.107 回答
10

究竟为什么这有未指定的行为。

当我第一次看这个例子时,我觉得这个行为定义得很好,因为这个表达式实际上是一组函数调用的简写。

考虑这个更基本的例子:

cout << f1() << f2();

这被扩展为一系列函数调用,其中调用的类型取决于运算符是成员还是非成员:

// Option 1:  Both are members
cout.operator<<(f1 ()).operator<< (f2 ());

// Option 2: Both are non members
operator<< ( operator<<(cout, f1 ()), f2 () );

// Option 3: First is a member, second non-member
operator<< ( cout.operator<<(f1 ()), f2 () );

// Option 4: First is a non-member, second is a member
cout.operator<<(f1 ()).operator<< (f2 ());

在最低级别,这些将生成几乎相同的代码,所以我从现在开始只参考第一个选项。

标准中保证编译必须在输入函数体之前评估每个函数调用的参数。在这种情况下,cout.operator<<(f1())必须在 is 之前进行评估operator<<(f2()),因为cout.operator<<(f1())调用其他运算符需要结果。

未指定的行为开始生效,因为尽管必须对运算符的调用进行排序,但对它们的参数没有这样的要求。因此,生成的订单可以是以下之一:

f2()
f1()
cout.operator<<(f1())
cout.operator<<(f1()).operator<<(f2());

或者:

f1()
f2()
cout.operator<<(f1())
cout.operator<<(f1()).operator<<(f2());

或者最后:

f1()
cout.operator<<(f1())
f2()
cout.operator<<(f1()).operator<<(f2());
于 2009-10-01T17:13:37.443 回答
2

未指定评估函数调用参数的顺序。简而言之,您不应该使用具有影响语句含义和结果的副作用的参数。

于 2009-10-01T15:53:23.883 回答
0

是的,根据标准,函数参数的评估顺序是“未指定”。

因此,不同平台上的输出不同

于 2009-10-01T15:53:06.287 回答
0

如前所述,您已经走进了未定义行为的闹鬼森林。要获得每次预期的效果,您可以消除副作用:

int myFunc(int &x) {
   int temp = x * x * x;
   return temp;
}

int main() {
   int x = 2;
   cout << myFunc(x) << endl << myFunc(x+1) << endl << myFunc(x+2) << endl;
   //Note that you can't use the increment operator (++) here.  It has
   //side-effects so it will have the same problem
}

或将函数调用分解为单独的语句:

int myFunc(int &x) {
   int temp = x * x * x;
   x += 1;
   return temp;
}

int main() {
   int x = 2;
   cout << myFunc(x) << endl;
   cout << myFunc(x) << endl;
   cout << myFunc(x) << endl;
}

第二个版本可能更适合测试,因为它迫使他们考虑副作用。

于 2009-10-01T16:01:48.233 回答
0

这就是为什么每次你写一个有副作用的函数时,上帝都会杀死一只小猫!

于 2009-10-01T17:49:30.707 回答