具有嵌入式函数调用的C ++输出评估顺序

时间:2009-10-01 15:46:32

标签: c++ gcc mingw

我是一个介绍C ++类的TA。上周在测试中询问了以下问题:

以下程序的输出是什么:

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的工作方式,输出必须是前进的,但似乎功能的评估也应该向前推进。

感谢您的帮助!

6 个答案:

答案 0 :(得分:15)

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

海湾合作委员会没有义务向你(或我)解释为什么要这样做。它可能是一个性能优化,可能是因为编译器代码出现了一些更短更简单的方式,可能是因为其中一个mingw编码员个人讨厌你,并且想要确保如果你做出的假设不是'由标准保证,您的代码出错了。欢迎来到开放标准的世界: - )

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

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

然后那将是未定义的行为。在这段代码中,编译器有效地评估所有三个“x ++”子表达式,然后是三次调用pow,然后开始对operator<<的各种调用。由于此顺序有效且没有将x的修改分开的序列点,因此结果完全未定义。在您的代码段中,仅指定了执行顺序。

答案 1 :(得分: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())之前评估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());

答案 2 :(得分:2)

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

答案 3 :(得分:0)

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

因此,不同平台的产出不同

答案 4 :(得分: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;
}

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

答案 5 :(得分:0)

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