#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])
и копирует их по адресу цели.