К>Все совершенно верно про неопределенное поведение.
К>Любая функция с побочным эффектом (а оператор автоинкремента таковым является) способна дать такой же результат.

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

Если заключить модификацию переменной 'z' в отдельную функцию

int incz(inr& z) { return ++z; }


и переписать вызов вот так:

a.f( incz(z1) , 1).f( incz(z1) , 2).f( incz(z1) , 3);


то теперь мы получим, что каждое изменение переменной 'z' "зажато" между парой точек следования — каждая функция имеет точку следования на входе и на выходе (в данном случае — функция 'incz'). Теперь у нас уже нет ситуации, когда один и тот же скалярный объект модифицируется несколько раз между парой соседних точек следования. Поэтому теперь у нас нет неопределенного поведения.

Но тем не менее, хотя неопределенного поведения у нас больше нет, предсказать результат выполнения этого кода все равно нельзя. Почему? Потому что теперь в игру вступают два других правила С++:

1) Порядок вычисления подвыражений в рамках одного выражения не определен.
2) Порядок вычичления параметров функции не определен.

(А может быть правило 2 — это просто частный случай правила 1). Не надо пугаться присутствия в этих правилах слов "не определен". К неопределеному поведению (undefined behavior) они никакого отношения не имеют.

Очень важно не путать ситуацию, когда выражение порождает неопределенное поведение, и когда выражение не порождает неопределенного поведения, но его результат зависит от порядка вычисления. Это — разные ситуации. Данный пример не очень хорошо иллюстрирует это разницу.

Чтобы получше ее проиллюстрировать, я приведу вот такой пример:

int a[] = { 1, 2, 3 };
int* p = a;
int s = *p++ + *p++ + *p++; // (1)


Чему будет равно 's'?

Часто можно услышать такие рассуждения по этому поводу: "Да, я знаю, что порядок вычисления подвыражений выражения (1) не определен. Но в данном случае, как ни верти, все равно получается одно и то же. Либо '1 + 2 + 3', либо '3 + 2 + 1', либо '2 + 1 + 3', либо еще как — всегда в сумме получается 6. Значит переменная 's' пронициализируется значением 6."

Когда же обнаруживается, что переменная 's' инициализируется не значением 6, начинаются разговоры про "глюки компилятора" и т.п. На самом деле компилятор ни в чем не виноват. Выражение (1) порождает неопределенное поведение, потому что оно содержит несколько модификаций одного скалярного объекта 'p' между двумя соседними точками следования. Неопределенное поведение — этим все сказано.

А теперь давайте модифицируем это пример вот так:

int getp(int*& p) { return *p++; }

int a[] = { 1, 2, 3 };
int* p = a;
int s = getp(p) + getp(p) + getp(p); // (2)


Теперь у меня модификация указателя делается внутри функции 'getp', т.е. каждая модификация "обрамлена" своей парой точек следования — на входе в функцию 'getp' и на выходе из нее. Теперь выражение (2) уже не порождает неопределенного поведения. Можно ли теперь сказать, чему будет равна переменная 's'. Да, можно. Она будет равна 6. Да, порядок вычисения подвыражений выражения (2) по-прежнему не определен. Но результат действительно не зависит от этого порядка! И '1 + 2 + 3', и '3 + 2 + 1', и все остальные варианты дают в результате именно 6.

Это пример очень хорошо иллюстрирует разницу между неопределенным поведением и неопределенным порядком вычисления подвыражений целого выражения.

По отношению к исходному примеру твое утверждение "Любая функция с побочным эффектом (а оператор автоинкремента таковым является) способна дать такой же результат" — неверно. Оператор инкремента, примененный к скалярному типу, функцией не является. Следствием этого является то, что модификации, производимые таким оператором инкремента, не отделены от "окружающего мира" при помощи точек следования. Этого достаточно для того, чтобы перейти тонкую границу и попасть в область неопределенного поведения. В случае функции с побочным эффектами этого бы не произошло. Мой пример это очень хорошо иллюстрирует.

К>Просто логика работы компилятора была, что называется, налицо.

К>И именно об этом я и написал.
К>Строится дерево выражения

Твои рассуждения могли бы быть перекрасной иллюстрацией логики компилятора, если бы мы имели дело со случаем, в котором поведение программы зависело бы от порядка вычисления подвыражений целого выражения. Именно это ты иллюстрируешь. Но в данном случае, еше раз повторюсь, мы имеем дело с на порядок более "запущенной" ситуацией — неопределенным поведением. Пытатсья объяснить логику компилятора здесь — занятие неблагодарное. Ее может просто не быть.
Автор: Андрей Тарасевич    Оценить