2009-10-01 1 views
9

Я TA для вводного класса C++. Следующий вопрос был задан на тест на прошлой неделе: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, но похоже, что оценка функций также должна быть направлена ​​вперед.

Благодарим за помощь!

ответ

15

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

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

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

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

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

+0

Спасибо за объяснение, я подумал, что это экземпляр неопределенной спецификации, но теперь я могу быть в этом уверен. И это может стать примером для студентов, почему побочные эффекты и чрезмерно сложное гнездование плохие :) –

+5

+1. И, в частности, обратите внимание, что программа не демонстрирует неопределенного поведения. 'x' все еще изменяется не более одного раза между двумя точками последовательности, поскольку выполнение вызова функции не может чередовать друг друга. Его поведение неуказано, и после выражения выражения cout значение 'x' должно быть 5. –

+0

@onebyone: Очень забавная часть« ненависти »... :) –

0

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

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

+0

Аналогичным образом std :: cout << x << ++ x << x ++; это неспецифицированное поведение. Примечание: оно не вызывает неопределенное поведение. –

+1

@Prasoon, нет в этом случае, он * делает * вызывать неопределенное поведение, потому что изменение 'x' не заключено в скобки с точкой последовательности друг к другу с заменой' x'. –

+1

@litb: Хорошее наблюдение! –

2

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

+0

Вам также нужно будет объяснить, почему (и как) '<<' на самом деле вызов функции - иначе ответ может быть трудно понять :) –

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

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

+0

Да, я уже упоминал им, что они могли видеть ожидаемое поведение, разбивая выходной оператор вроде этого. Я согласен с тем, что у меня нет функции с побочными эффектами, но, как вы отметили, этот вопрос очень конкретно предназначен для проверки их знаний о таких побочных эффектах. Это и как TA я не могу изменить вопросы, просто объясню их :) –

+1

Первый код «fix» не приведет к ожидаемому поведению вообще. Оценка отдельных подвыражений по-прежнему не определена, и вы ввели дополнительную проблему: вы передаете временные данные неконстантной ссылке. –

+1

Просто nit, о котором упоминалось в других ответах, но технически это неопределенное поведение, а не неопределенное поведение. Неопределенное поведение означает, что все может произойти, включая форматирование вашего жесткого диска или запуск бездействия. Unspecified означает, что он может оценивать аргументы в любом порядке, который ему нравится, но все они должны быть оценены до вызова функции. – KeithB

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

Nice :) Я собирался написать что-то подобное, но мне потребовалось некоторое время, чтобы проверить, что именно гарантирует, что различные '' '' '' '' вызовы выполняются последовательно. Поскольку вы заявили, что это имеет смысл для меня, с тех пор подразумеваемый аргумент объекта рассматривается только как нормальный аргумент функции, который оценивается до точки ввода функции. Я думал, что этот аргумент существует только для целей перегрузки, но кажется, что он имеет значение и для этих игр побочных эффектов. +1 :) –

0

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