С++ Выходной порядок оценки со встроенными вызовами функций

Я - 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

Когда я запускаю его в своей Linux-среде с помощью gcc, я получаю то, что ожидаю. Используя MinGW на моей машине с Windows, я понимаю, о чем они говорят. Кажется, что сначала вычисляется последний вызов myFunc, затем второй вызов, а затем первый, а затем, когда он имеет все результаты, он выводит их в обычном порядке, начиная с первого. Но поскольку вызовы были сделаны не по порядку, числа противоположны.

Мне кажется, что это оптимизация компилятора, позволяющая оценивать вызовы функций в обратном порядке, но я действительно не знаю почему. Мой вопрос: правильны ли мои предположения? Это то, что происходит в фоновом режиме? Или что-то совершенно другое? Кроме того, я действительно не понимаю, почему было бы полезно оценить функции назад, а затем оценить выход вперед. Результат должен быть перенаправлен из-за того, как работает ostream, но похоже, что оценка функций также должна быть направлена ​​вперед.

Спасибо за вашу помощь!

Ответ 1

Стандарт С++ не определяет, какой порядок оценивают подвыражения полного выражения, за исключением некоторых операторов, которые вводят порядок (оператор запятой, тернарный оператор, короткозамкнутые логические операторы) и тот факт, что выражения, которые составлять аргументы/операнды функции/оператора, все оцениваются перед самим оператором /.

GCC не обязан объяснять вам (или мне), почему он хочет заказать их так, как он. Это может быть оптимизация производительности, возможно, потому, что код компилятора вышел на несколько строк короче и проще, так как это может быть из-за того, что один из mingw-кодеров лично вас ненавидит и хочет убедиться, что если вы сделаете предположения, t гарантируется стандартом, ваш код идет не так. Добро пожаловать в мир открытых стандартов: -)

Изменить для добавления: litb делает точку ниже о (un) определенном поведении. В стандарте говорится, что если вы изменяете переменную несколько раз в выражении и если существует допустимый порядок оценки для этого выражения, такой, что переменная изменяется несколько раз без точки последовательности между ними, тогда выражение имеет undefined поведение. Это не применяется здесь, потому что переменная изменена в вызове функции, и там есть точка последовательности в начале любого вызова функции (даже если компилятор строит ее). Однако, если вы вручную ввели код:

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

Тогда это будет поведение undefined. В этом коде для компилятора достаточно оценить все три подвыражения "x ++", затем три вызова на pow, а затем начать с различных вызовов на operator<<. Поскольку этот порядок действителен и не имеет точек последовательности, разделяющих модификацию x, результаты полностью undefined. В фрагменте кода не указан только порядок выполнения.

Ответ 2

Именно поэтому это неуказанное поведение.

Когда я впервые посмотрел на этот пример, я почувствовал, что поведение четко определено, потому что это выражение на самом деле является короткой рукой для набора вызовов функций.

Рассмотрим этот более простой пример:

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());

Ответ 3

Порядок оценки параметров вызова функции не указан. Короче говоря, вы не должны использовать аргументы, которые имеют побочные эффекты, которые влияют на смысл и результат выражения.

Ответ 4

Да, порядок оценки функциональных аргументов "Unspecified" в соответствии со стандартами.

Следовательно, выходы различаются на разных платформах.

Ответ 5

Как уже было сказано, вы бродили в призрачном лесу поведения undefined. Чтобы получить то, что ожидается каждый раз, когда вы можете либо удалить побочные эффекты:

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;
}

Вторая версия, вероятно, лучше для теста, поскольку она заставляет их учитывать побочные эффекты.

Ответ 6

И вот почему каждый раз, когда вы пишете функцию с побочным эффектом, Бог убивает котенка!