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

Eri*_*fka 9 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;
}
Run Code Online (Sandbox Code Playgroud)

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

8
27
64
Run Code Online (Sandbox Code Playgroud)

但是现在有几个学生已经指出,当他们在某些环境中运行时他们实际上是相反的:

64
27
8
Run Code Online (Sandbox Code Playgroud)

当我在我的Linux环境中使用gcc运行它时,我得到了我期望的结果.在我的Windows机器上使用MinGW,我得到了他们正在谈论的内容.它似乎是先评估对myFunc的最后一次调用,然后是第二次调用然后是第一次调用,然后一旦它具有所有结果,它就按正常顺序输出它们,从第一次开始.但由于呼叫是按顺序进行的,因此数字相反.

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

谢谢你的帮助!

Ste*_*sop 15

C++标准没有定义什么顺序的完整表达式的子表达式进行评估,除了某些运营商,其引入的顺序(逗号运算符,三元运算,短路逻辑运算符),而事实上,该表达式补函数/运算符的参数/操作数都在函数/运算符本身之前进行求值.

海湾合作委员会没有义务向你(或我)解释为什么要这样做.这可能是一个性能优化,它可能会因为编译器代码出来几行更短,更简单的这种方式,它可能是因为MinGW的程序员的一个个人恨你,并希望确保,如果你做的假设不是活得由标准保证,您的代码出错了.欢迎来到开放标准的世界:-)

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

std::cout << pow(x++,3) << endl << pow(x++,3) << endl << pow(x++,3) << endl;
Run Code Online (Sandbox Code Playgroud)

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

  • +1.特别要注意的是,程序没有表现出未定义的行为.`x`仍然只在两个序列点之间最多更改一次,因为函数调用执行不能相互交错.它的行为是未指定的,在cout表达式语句之后,`x`的值必须为5. (5认同)

Ric*_*den 10

究竟为什么这有不明确的行为.

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

考虑这个更基本的例子:

cout << f1() << f2();
Run Code Online (Sandbox Code Playgroud)

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

// 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 ());
Run Code Online (Sandbox Code Playgroud)

在最低级别,这些将生成几乎相同的代码,因此我将仅提及从现在开始的第一个选项.

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

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

f2()
f1()
cout.operator<<(f1())
cout.operator<<(f1()).operator<<(f2());
Run Code Online (Sandbox Code Playgroud)

要么:

f1()
f2()
cout.operator<<(f1())
cout.operator<<(f1()).operator<<(f2());
Run Code Online (Sandbox Code Playgroud)

或者最后:

f1()
cout.operator<<(f1())
f2()
cout.operator<<(f1()).operator<<(f2());
Run Code Online (Sandbox Code Playgroud)