Страницы

Поиск по вопросам

суббота, 7 декабря 2019 г.

О порядке вычисления выражений

#cpp #cpp11 #неопределенное_поведение


Хотелось бы разобраться какими правилами определяется порядок вычисления значений
выражений в общем случае.

Допустим, есть такой код

int readValue()
{
  int v;
  cin >> v;
  return v;
}

int main()
{
  cout << readValue() << ' ' << readValue() << '\n';
  return 0;
}


Как известно, оператор побитого сдвига вычисляется слева на право - но при вводе
1 2 вывод 2 1 (компилятор от майкрософта), с чем это связано ? 

порождает ли данный код undefined behaviour или здесь всего лишь порядок вывода неопределён ?

Если убрать код чтения - всё довольно предсказуемо

int readValue(int v)
{
  return v;
}

int main()
{
  cout << readValue(1) << ' ' << readValue(2) << '\n';
  return 0;
}


вывод - 1 2

в чем дело ?
    


Ответы

Ответ 1



Согласно стандарту C++ порядок вычисления аргументов функции не специфицирован, что означает, что компиляторы могут выбрать любой порядок вычисления аргументов Из стандарта C++ (1.9 Program execution) 3 Certain other aspects and operations of the abstract machine are described in this International Standard as unspecified (for example, order of evaluation of arguments to a function). Данное предложение cout << readValue() << ' ' << readValue() << '\n'; представляет собой цепочку вызовов функций operator <<. Оно соответствет следующей цепочки вызовов функций с именем operator << operator <<( operator <<( std::cout.operator <<( readValue() ), ' ' ) .operator <<( readValue() ), '\n' ); MS VC++ вычисляет аргументы справа налево. Другой компилятор может вычислять аргументы в другом порядке, например, слева направо. EDIT: Я приведу дополнительный наглядный пример на основе перегрузки оператора operator &&. Рассмотрите следующую демонстрационную программу #include struct A { int x; A( int x = 0 ) : x( x ) {} }; bool operator &&( const A &a, int x ) { return 0 < a.x && 0 < x; } A f1() { std::cout << "f1()" << std::endl; return A( -1 ); } int f2() { std::cout << "f2()" << std::endl; return 1; } int main(void) { std::cout << ( f1() && f2() ) << std::endl; return 0; } Ее вывод на консоль следующий f2() f1() 0 Если бы это был встроенный оператор operator && для фундаментальных типов, то выражение f2() не вычислялось бы в случае, если выражение f1() имело значение false. Однако так как здесь имеет место вызов перегруженной пользователем функции, то порядок выполняемых действий компилятором следующий. Компилятор сначала пытается определить, какая именно из перегруженных функций используется. Если он такой не находит, или имеет место неоднозначность, то компилятор выдает сообщение об ошибке. Если он находит такую функцию, то он заменяет данную запись на вызов соответствующей функции пользователя. И на основе этого вызова функции строит результирующий объектный код. Так как порядок вычисления аргументов функции не специфицирован, то, как показывает вывод, компилятор сначала вычислил правый аргумент, а затем левый. Сравните вывод данной программы с выводом программы, в которой оператор operator &&имеет дело с фундаментальными типами, то есть когда используется встроенный оператор operator &&. #include int f1() { std::cout << "f1()" << std::endl; return 0; } int f2() { std::cout << "f2()" << std::endl; return 1; } int main(void) { std::cout << ( f1() && f2() ) << std::endl; return 0; } Вывод программы на консоль f1() 0 Как видно, функция f2 никогда не будет вызвана, так как значение левого операнда равно false. Это различие связано с тем, что в первом случае компилятор имеет дело с вызовом именно пользовательской функции с именем operator &&,а во втором случае имеет дело со встроенным оператором operator &&.

Ответ 2



Утверждение о том, что данные операторы побитового сдвига вычисляются слева-направо - верно, но оно говорит лишь о порядке применения последовательных операторов << к их непосредственным операндам. В данном случае непосредственными операндами операторов << являются некие временные промежуточные значения типа int cout << __tmp_int1 << ' ' << __tmp_int2 << '\n'; Вот об этом (и только об этом) выражении в данном случае можно говорить, что оно вычисляется слева-направо. То есть значение __tmp_int1 будет выведено раньше, а значение __tmp_int2 - позже. А что касается порядка и момента подготовки этих промежуточных значений - оно никак не ограничивается вышепроцитированным утверждением. То есть компилятор может сделать и int __tmp_int1 = ReadValue(); int __tmp_int2 = ReadValue(); и наоборот int __tmp_int2 = ReadValue(); int __tmp_int1 = ReadValue(); или вообще переплести подготовку __tmp_int1 и __tmp_int2 с вызовами << неким непредсказуемым образом. Главное лишь, чтобы значение было готово к тому моменту, когда оно нужно. Более ничего не оговаривается.

Ответ 3



Как уже говорилось, что порядок вычисления аргументов функций и операторов не определён стандартом, но в абсолютном числе случаев, он идёт справа налево по практическим соображениям: предположим есть код void f(int, int); f(g(), h()); h() удобнее вычислять первой, так как её результат надо раньше закладывать в стек для вызова функции f(). Ну, а с примером int readValue(int v) { return v; } int main() { cout << readValue(1) << ' ' << readValue(2) << '\n'; return 0; } Всё тоже просто. На самом деле тут компилятор скорее всего вычислил значения readValue(1) и readValue(2) на этапе компиляции, т.е. скорее всего в ассемблерном коде даже не было вызовов readValue. Собственно вот какой ассемблерный код сгенерился в vs2015: std::cout << readValue(1) << ' ' << readValue(2) << std::endl; 01121000 mov ecx,dword ptr [_imp_?cout@std@@3V?$basic_ostream@DU?$char_traits@D@std@@@1@A (0112203Ch)] 01121006 push offset std::endl > (01121300h) 0112100B push 2 0112100D push 1 0112100F call dword ptr [__imp_std::basic_ostream >::operator<< (01122038h)] 01121015 mov ecx,eax 01121017 call std::operator<< > (011210F0h) 0112101C mov ecx,eax 0112101E call dword ptr [__imp_std::basic_ostream >::operator<< (01122038h)] 01121024 mov ecx,eax 01121026 call dword ptr [__imp_std::basic_ostream >::operator<< (01122058h)] return 0; 0112102C xor eax,eax } 0112102E ret Хотя тут результат не зависит от порядка вызова, без оптимизации тоже самое будет.

Комментариев нет:

Отправить комментарий