Страницы

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

пятница, 29 ноября 2019 г.

Какое значение примет элемент n[1] после выполнения команд:

#c++ #language_lawyer #неопределенное_поведение


int i = 0, n[] = {7, 5, 3, 1};
for ( ; i<3; n[i++] = n[i]);


Дело в том, что два разных компилятора (Code Blocks и CppDroid) выдают два разных
значения. В Code Blocks получается 5, а в CppDroid - 3. Так какой же ответ правильный?
Проблема в одном из компиляторов, или само задание некорректно?
    


Ответы

Ответ 1



По-моему, порядок вычисления выражения слева и выражения справа при выполнении присваивания не оговорен стандартом (порядок определяется конкретным компилятором). Для C++11, раздел 5.17: The assignment operator (=) and the compound assignment operators all group right-to-left. All require a modifiable lvalue as their left operand and return an lvalue referring to the left operand. The result in all cases is a bit-field if the left operand is a bit-field. In all cases, the assignment is sequenced after the value computation of the right and left operands, and before the value computation of the assignment expression. With respect to an indeterminately-sequenced function call, the operation of a compound assignment is a single evaluation Поскольку итоговый результат вычисления зависит от порядка вычисления операндов, то и возникает undefined behavior. Для C++17, раздел 5.18, порядок вычисления уже более строго определен: The right operand is sequenced before the left operand. Поэтому неопределенного поведения не должно возникать. И после выполнения цикла массив не изменится, поэтому n[1] равно 5.

Ответ 2



Компилятор C++ имеет право переупорядочивать инструкции в целях оптимизации. Рассмотрим выражение n[i++] = n[i] Как оно может быть интерпретировано? Первый вариант. int tmp = i; i = i + 1; n[tmp] = n[i]; То есть, например, при i == 0 n[0] = n[1] Второй вариант auto tmp_n = n[i]; int tmp_i = i; i = i + 1; n[tmp_i] = tmp_n; То есть, при i == 0 n[0] = n[0]; Таким образом, поскольку порядок вычислений внутри одной операции не определён, компилятор может сгенерировать как первый код, так и второй. То есть правильный ответ здесь следующий: Неопределено. P.S. Кстати, Code::Blocks это не компилятор, а среда разработки, не имеющая собственного компилятора.

Ответ 3



Имеет место неопределенное поведение программы. Согласно стандарту C++ (1.9 Program execution) ...If a side effect on a scalar object is unsequenced relative to either another side effect on the same scalar object or a value computation using the value of the same scalar object, and they are not potentially concurrent (1.10), the behavior is undefined. Там же в стандарте приведен схожий пример i = i++ + 1; // the behavior is undefined

Ответ 4



Никакой. Фрагмент n[i++] = n[i] провоцирует неопределённое поведение, так как операция приравнивания не является точкой следования. Это значит, что оператор волен выбирать, какое именно выражение надо вычислять первее: n[i++] или n[i]. Единственное ограничение — к моменту присваивания оба фрагмента должны дать по значению (ссылку и число соответственно). Неопределённое поведение следует из того, что компилятор рассматривает обе части независимо. Соответственно, i++ из левой части и i из правой друг с другом как бы не связаны. Мало того, что чтение-изменение-запись постинкремента может быть переставлено местами с просто чтением, так они могут быть ещё и перемешаны друг с другом. На большом StackOverflow уже был задан вопрос о том, почему присваивание не является точкой следования: Имеется ли какое-нибудь обоснованию тому, что оператор = не является точкой следования как в Си, так и в C++? Нужна веская причина для того, чтобы что-то стало точкой следования. В причинах же того, чтобы этого не делать, нужды нет — это вариант по умолчанию. К примеру, && должен быть точкой следования из-за short-circuiting: если левая часть оператора ложна, правая его часть вычислена не будет. Это связано не столько с оптимизацией, сколько с возможностью создания зависимости правой части от левой (к примеру, в ptr && ptr->data). Поэтому левая часть обязательно должна быть вычислена строго до правой, чтобы знать, надо ли вычислять правую часть вообще. В случае же с = подобной причины не существует. Хотя этот оператор и является присваиванием (...), точный порядок вычисления сторон не имеет значения, пока они выполняются до собственно присваивания. P. S. Кстати, проблема с ++i + ++i имеет такую же первопричину.

Ответ 5



Неопределенное поведение. Компилятор g++, запущенный с ключем -Wall честно предупреждает об этом. Пример (с чуть модифицированной для печати промежуточных результатов программой): avp@wubu:hashcode$ cat t.c #include #include #include int main (int ac, char *av[]) { int i = 0, n[] = {7, 5, 3, 1}; int l = 0; for ( ; i<3; n[i++] = n[i]) { printf("loop %d i = %d\n", l++, i); for (int j = 0; j < 4; j++) printf("%d ", n[j]); puts(""); } puts(""); printf("result i = %d\n", i); for (int j = 0; j < 4; j++) printf("%d ", n[j]); puts(""); } avp@wubu:hashcode$ g++ -Wall t.c t.c: In function ‘int main(int, char**)’: t.c:11:28: warning: operation on ‘i’ may be undefined [-Wsequence-point] for ( ; i<3; n[i++] = n[i]) { ^ avp@wubu:hashcode$ ./a.out loop 0 i = 0 7 5 3 1 loop 1 i = 1 5 5 3 1 loop 2 i = 2 5 3 3 1 result i = 3 5 3 1 1 avp@wubu:hashcode$ Как видите n[1] = 3, т.е. этот компилятор в операторе присваивания (в данном случае это завершающая часть for(;;)) берет текущее значение i, вычисляет и запоминает адрес цели, увеличивает i, на основе уже нового значения i выбирает данные источника (n[i]) и копирует их по адресу цели.

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

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