C ++ Порядок вычисления вывода со встроенными вызовами функций - PullRequest
9 голосов
/ 01 октября 2009

Я ТА для начального класса 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

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

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

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

Ответы [ 6 ]

15 голосов
/ 01 октября 2009

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

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

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

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

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

10 голосов
/ 01 октября 2009

Точно, почему это имеет неопределенное поведение.

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

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

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 голосов
/ 01 октября 2009

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

0 голосов
/ 01 октября 2009

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

0 голосов
/ 01 октября 2009

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

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

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

0 голосов
/ 01 октября 2009

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

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

...