вторник, ноября 15, 2005

Точки следования (sequence points)

Их не рассматривают на занятиях, о них редко пишут в книгах. Тем не менее любой, кто пишет на C++, с ними сталкивается постоянно в ходе своей работе. И если знать о них чуть больше, многие из вещей, казавшихся раньше таинственными, становятся очевидными.

О таинственном. Я пишу программу
int x=0;
x=x++;
cout<<x;

Вопрос: что выведется на экран? Ну что может быть проще - сейчас запустим да проверим. Я запустила MSVC++6.0, ответ получился 1.
Потом запустила gcc 3.4.4 и получился 0.
Если бы запустила что-то еще, то могло получиться нечто третье...
Кто из них прав? На самом деле все. А вот почему так в двух словах и не скажешь...

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

Точки следования (sequence points) - это некие точки в программе, где состояние реальной программы полностью соответствует состоянию абстрактной машины, описанной в Стандарте. С помощью точек следования стандарт объясняет, что может, а чего не может делать компилятор и что нам нужно сделать, чтобы написать корректный код.

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

Если код выглядит так:
int x;
x = 3;
cout << x << endl;
x = 4;

То нет никаких сомнений, что на печать будет выведено 3. Точка с запятой в конце выражения "x = 3;" как и любая точка с запятой в конце любого выражения - это точка следования. Перед тем как любой код после этой точки с запятой выполнится, побочный эффект от сохранения значения 3 в int объект x должен быть полностью завершен. И мы точно знаем, что вывод не будет равен 4, потому что вывод полностью завершен к тому моменту, как выполнение программы достигнет точки с запятой в конце выражения "cout << x << endl;". Побочный эффект от сохранения значения 4 в x в следующем выражении еще не случился.

Где находятся точки следования


  1. В конце каждого полного выражения(Глава Стандарта 1.9/16). Обычно они помечены точкой с запятой ;

  2. В точке вызова функции (1.9/17). Но после вычисления всех аргументов. Это и для inline функций в том числе.

  3. При возвращении из функции. (1.9/17) Есть точка следования сразу после возврата функции, перед тем как любой другой код из вызвавшей функции начал выполняться.

  4. (1.9/18) После первого выражения (здесь оно называется 'a') в следующих конструкциях:

    a || b

    a && b

    a , b

    a ? b : c

    Вычисление здесь идет слева направо. То есть левое выражение (по имени 'a') вычисляется и все побочные эффекты от такого вычисления завершаются. Потом, если все значение всего выражения известно, правое выражение не вычисляется, иначе вычисляется.
    Правило слево-направо не работает для переопределенных операторов. В этом случае переопределенный оператор ведет себя как обычная функция. Еще такой момент - ?: не может быть переопределен по стандарту.

    (Упомянутая запятая это оператор запятая. Она не имеет никакого отношения к запятой, разделяющей аргументы функции.)


Если рассмотреть пример "x = x++;", то здесь имеет место попытка изменить объект дважды.
Если программа пытается модифицировать одну переменную дважды не пересекая точку следования, то это ведет к undefined behavior. Так говорит Стандарт. И тут же в Стандарте приводятся примеры.

1) i = v[i++]; //unspecified behavior
2) i = 7, i++, i++; //i равно 9

3) i = ++i + 1; //unspecified behavior
4) i = i + 1; //значение i увеличено на 1

Что-то с чем-то не сходится. В первом и третьем примерах указано "unspecified behavior", хотя переменная изменяется дважды и только что было сказано, что это ведет к undefined behavior.
И точно в списке багов Стандарта это место упомянуто. В предложении по устранению бага советуют поменять слово unspecified на слово undefined.
Но на самом деле не так уж и важно, unspecified или undefined behavior. Важно то, что непонятно, что именно получится в итоге. Поэтому не надо модифицировать переменную больше одного раза между двумя точками следования, поскольку это может привести к весьма неожиданным последствиям. Вот, например
int x;
x = 3;
cout << (++ x + ++ x) + ++ x << endl;
x = 3;
cout << ++ x + (++ x + ++ x) << endl;

gcc 3.4.4:
16
18
Получилось, что операция сложения не ассоциативна!

MSVC++6.0:
18
18

Примеры:
i = ++i; // undefined behavior, переменная модифицируется дважды
i = i + 1; // все в порядке
i ? i=1 : i=5; // все в порядке (там, где знак ? есть точка следования, а потом выполнится лишь одно из выражений)
i=1; i++; // все в порядке (после каждого выражения находится точка следования)
i=1, i++; // все в порядке (на операторе запятая находится точка следования)

Пример с функциями:
void f(int, int);
int g();
int h();
f(g(), h());
По Стандарту неизвестно, какая из функций g или h будет вызвана первой, но известно, что f() будет вызвана последней.


Разработчикам gcc периодически сабмитят баги по этому поводу, в итоге в
Frequently Reported Bugs in GCC было внесено:
Результат следующих операций непредсказуем и это полностью соответствует стандарту
x[i]=++i
foo(i,++i)
i*(++i) /* special case with foo=="operator*" */
std::cout << i << ++i /* foo(foo(std::cout,i),++i) */

Вот пример такой баги: Bug13403. Их таких там очень много.

В следующей программе
#include <iostream>
int main(){
int i=0;
i = 6+ i++ + 2000;
std::cout << i << std::endl;
return 0;
}

Результат единица! А должен быть 2006.
Если убрать "i++", то результат правильный ("2006"). Но пока есть "i++" внутри выражения, я могу делить, умножать, вычитать, складывать, все равно результат всегда "1".
Например в выражении
i = (6+ i++ + 2000)/2;
"i" все равно единица.

Но если я заменю постфикс "i++" префиксом "++i" тогда все считается корректно.


Ну отвечают на это все - "это undefined behavior по Стандарту" и баг закрывают.

Несколько присваиваний подряд
Интересный момент с таким кодом:
int a=1, b=2, c=3;
a=b=c=0;
Стандарт как-то очень нечетко описывает такую ситуацию. Он говорит, что операция должна происходить справа налево, но ничего не говорит дополнительно по поводу того, когда должен быть результат этой операции записан в переменную. Точек следования внутри выражения нет, что значит, что компилятор может теоретически творить здесь что угодно и не обязательно все переменные в итоге будут равны 0. Так что, опять же теоретически, здесь имеет место unspecified behavior. По этому поводу периодически рождаются хвостатые флеймы в comp.lang.c++.moderated. Но на самом деле я не слышала о компиляторе, который делал бы в такой ситуации присвоение не справа налево. Вопрос этот известен, а компиляторы не садисты пишут.

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

Ссылки по теме:
Разница между unspecified behavior и undefined behavior
comp.lang.c++.moderated Undefined behavior?? I think not!
comp.lang.c++.moderated postfix increment and assignment
comp.lang.c++.moderated fresh look on copying objects and passing args by value
comp.lang.c++.moderated Evaluation precedence questions
comp.lang.c++.moderated = Operator?
comp.lang.c++.moderated How defined is undefined behaviour?
comp.lang.c++.moderated Case for post-increment/decrement
comp.lang.c++.moderated What is a sequence point?
comp.lang.c++.moderated Sequence point and evaluation order
comp.lang.c++.moderated sequence point and reference
comp.lang.c++.moderated sequence points and , operator

65 коммент.:

Анонимный комментирует...

Ага, подробно не разбирали. Ну, просто говорили, что "неопределенное поведение" (запомните это, и никогда так не делайте), а точки следования, да, не упоминали.
Забавно другое: что вопросы вида
i++ + ++i (написанные i+++++i) или что значит ++i++ (тут, правда, уже и другое примешивается) часто задавали на собеседованиях. При этом услышать хотели вовсе не "неопределенное поведение" (и тем более не точки следования), а именно про префиксный и постфиксный инкременты, порядок следования и т.д. И даже если скажешь: поведение неопределенное, все равно нужен был ответ ("ну компилятор же все-таки это как-то вычислит, вот и напишите про порядок операций и т.д. и т.д.").
Впрочем, это уже о не совсем корректных задачах на собеседованиях на работу...
// tensor

Анонимный комментирует...

Все замечательно, только ты слово "undefined" несколько раз написала как "underfined". Просто обращаю внимание :)

Ivan Sagalaev комментирует...

Когда я вижу слово "Стандарт" - вот так с большой буквы, то у меня непреодолимо напрашиваются ассоциации, что это надо читать как "Святая Библия" :-)

А "Глава Стандарта 1.9/16" и вообще подозрительно смахивает на "Иезекиль, 25:17" :-)

Alena комментирует...

2tensor:
А, кстати, интересно, что делать в таких ситуациях? Упорствовать или сказать то, что хочет услышать собеседник? Я думаю, я бы вежливо, но твердо, настояла на своем...
Да и я знаю, здесь есть люди, которые сами проводят собеседования. Как бы вы отнеслись к тому, что соискатель указал вам на ваше давнее заблуждение?

2Sergey Petrov:
Спасибо, поправила.

2Maniac:
Ладно тебе зубоскалить, ты так же относишься к веб-стандартам. :-)

Анонимный комментирует...

А, кстати, интересно, что делать в таких ситуациях?
Упорствовать или нет - сложно сказать, особенно если собеседование. Мне приходилось в инете поправлять высказывания или статьи - так даже давая ссылки на источники, убедительные примеры, замечаю, что большинство ну оочень неохотно признает ошибки. А на собеседовании, где и книжки может не быть (куда ткнуть), и к интернету скорее всего не пойдут искать и проверять...
но выводы у меня твердо сделаны: если человек ошибается в том, о чем сам спрашивает соискателя (о чем сам заговорил), то такая работа пойдет лесом. Ибо с такими людьми дальше только хуже будет.

Как бы вы отнеслись к тому, что соискатель указал вам на ваше давнее заблуждение?
Да, да, смелее! Все дружно признаемся, как умеем адекватно реагировать на свои собственные ошибки ;)
// tensor

Анонимный комментирует...

Угу.
Заказчик недавно тестил наш компилятор. Прислал баг-репорт со следующим текстом:

for (int i=0; i<5; i=i++)
// do something

и долго ругался, что зацикливается это. Пришлось все в посте описанное объяснять тестеру. ;)

Alex Shubert комментирует...

Мой любимый тест. Меня так самого 5 лет назад принимали.
Испытуемому задается вопрос. Он отвечает. После чего вы с начальственным апломбом указываете на мнимую ошибку.

Если тут же согласится или начнет греться и кипятиться - резюме в мусорку.
Наиболее интересны два типа: те, которые, по глазам видно, твердо уверены в собственной правоте, но говорят, нечто вроде "надо свериться с литературой" и те, которые указывают, где можно найти верный ответ / предлагают провести эксперимент.
Последний, как правило, тут же получает работу.

Прямо на собеседовании меня на ошибках пока не ловили... избегаю спрашивать то, в чем не уверен

Анонимный комментирует...

По-моему, со случаем
a = b = c = d;
всё вполне прозрачно, потому что это

a::operator = (b::operator = (c::operator = (d)));

Анонимный комментирует...

после прочтения сего замечательного поста вспомнил про вопрос, который давно не давал мне покоя. если взять код
int i = 0;
std::cout << i++ << i++ << i++;
то по своей сути, он представляет из себя
int i = 0;
( ( std::cout.operator<<(i++) ).operator<<(i++) ).operator<<(i++);
т.е. если и вызов функции, и возврат из нее есть точки следования, то этот код валиден и должен выводить "012", однако он выводит "210" на всех доступных мне платфорах.
внимание, вопрос: это я идиот и так и должно быть? или компиляторы глючат?

elide@bk.ru

Alena комментирует...

т.е. если и вызов функции, и возврат из нее есть точки следования, то этот код валиден и должен выводить "012"

У меня в посте есть аналогичный пример с сайта gcc.
std::cout << i << ++i

Что раскладывается в
foo(foo(std::cout,i),++i)

где функция foo в данном случае - это operator<<.

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

или компиляторы глючат?

Если код проверен на нескольких компиляторах, то вряд ли они все будут глючить, причем совершенно одинаково. Плюс, есть же списки известных багов. В случае gcc, у них вообще доступ в багтрак открыт для всех желающих, подавляющее большинство багов известно. http://gcc.gnu.org/bugzilla/
Можно, конечно, напороться на нечто, что еще никто не находил, но вероятность очень невелика.

Анонимный комментирует...

Здравствуйте, Алёна! Не подскажете, ссылку на место в Стандарте, где написано, что "Правило слево-направо не работает для переопределенных операторов". Верю, но вот найти пока не могу :(.

Alena комментирует...

Здравствуйте, Алёна!

Приветствую!

Не подскажете, ссылку на место в Стандарте, где написано, что "Правило слево-направо не работает для переопределенных операторов". Верю, но вот найти пока не могу :(.

Прямо такой фразы дословно там нет.
Там так: в пятой главе рассказывается о встроенных операторах, где говорится, что для них правило "слева направо" выполняется.
В главе 13.5 рассказывается о перегрузке операторов. Где упоминается, что перегрузка осуществляется с помощью operator function. Поскольку это уже функции, то и правила для них работают те, что для функций, а не те, что для встроенных операторов. И там не только правило "слева направо" перестает работать. Например, если перегрузить оператор ++, то на нем появится точка следования.

Alena комментирует...

2Spalex:

Только что совершенно случайно наткнулась: очень подробное и хорошее объяснение про эти операторы есть у Мейерса в More Effective C++.
Item 7: Never overload &&, ||, or ,.

Анонимный комментирует...

Спасибо за наводочку! ;)

Анонимный комментирует...

Кстати, если серьезно к этому подходить: ++ ++ iter — тоже UB! :-)

Роман.

P. S. Мне почему-то немецкий интерфейс подсунули… Ну и что, что IP из Германии?

P. P. S. Шлите мне спам: dodge_this@qwertty.com

Unknown комментирует...

Аллёна! Ты мой куммир ! Ты раскрыла людям глаза ! Теперь фсе будут знать, что такое точки следования в С !

Анонимный комментирует...

>a=b=c=0;

c=0 - это выражение, значение которого равно нулю. точно такое же, как например ++i

archimed7592 комментирует...

Для ++i в Стандарте явно сказано, что результатом будет новое значение. Что же касается оператора присваивания, то этого там вовсе не сказано - сказано, что результатом будет сохранённое значение. Когда же происходит сохранение(side effect) - неизвестно(точнее известно, но не с достаточной точностью, чтобы гаарнтировать результат).
Т.о. даже очень часто используемая конструкция
if ((result = f()) == -1) { ... }
Представляет из себя UB(IS-5/4).

Для неверующих вот цитата из Стандарта про оператор присваивания:
IS-5.17/1 Assignment operators
There are several assignment operators, all of which group right-to-left. All require a modifiable lvalue as their left operand, and the type of an assignment expression is that of its left operand. The result of the assignment operation is the value stored in the left operand after the assignment has taken place; the result is an lvalue.

Насчёт того, когда именно "assignment" будет "taken place" читаем IS: 1.9/7-18, 5/1-4 или читаем пост Алёны про sequence points и последовавшую за ним дискуссию.

Анонимный комментирует...

to archimed7592

Так и не важно когда именно произойдёт присваивание. Стандарт требует, чтобы результатом выражения всегда было то значение, которое присвоится. Тут не сказано, что будет взято значения из левого операнда [в неизвестно какой момент], а чётко указано, что будет использоваться значение, которое будет иметь левый операнд после присвоения.

Alena комментирует...

2Анонимный:
Так и не важно когда именно произойдёт присваивание. Стандарт требует, чтобы результатом выражения всегда было то значение, которое присвоится.

Я предлагаю не продолжать эту дискуссию, потому что продолжать ее можно бесконечно. Чтобы увидеть всю аргументацию обеих сторон можно почитать любой флейм на comp.lang.c++.moderated. Ссылки в посте есть.

Анонимный комментирует...

x +=1; //?
x =++x; UB
x =x+1; //Норма

Почему во втором случае UB, а в 3 норма и как трактовать 1 вариант?
ведь во всех 3 случаях логически одно и тоже (сложение потом присваивание)

И как можно трактовать вот это выражение?
int x =-1;
if (++x) x=x
else x=-x;

если такой вариант вполне gприемлем
x =(++x)?x:-x;

Alena комментирует...

x +=1; //?
x =++x; UB
x =x+1; //Норма

Почему во втором случае UB, а в 3 норма и как трактовать 1 вариант?
ведь во всех 3 случаях логически одно и тоже (сложение потом присваивание)


Нет, во втором случае попытка изменить переменную x дважды. ++x - тоже подразумевает присваивание.

Первый вариант - просто прибавить единицу. Модификация одна.

И как можно трактовать вот это выражение?
int x =-1;
if (++x) x=x
else x=-x;


Эээ... А что не так с выражением?

Анонимный комментирует...

И как можно трактовать вот это выражение?
int x =-1;
if (++x) x=x
else x=-x;

Эээ... А что не так с выражением?

А что не так - то?

Анонимный комментирует...

В следующей программе
#include .....
int main(){
int i=0;
i = 6+ i++ + 2000;
std::cout << i << std::endl;
return 0;
}

Результат единица! А должен быть 2006.
Если убрать "i++", то результат правильный ("2006"). Но пока есть "i++" внутри выражения, я могу делить, умножать, вычитать, складывать, все равно результат всегда "1".
Например в выражении
i = (6+ i++ + 2000)/2;
"i" все равно единица.

Но если я заменю постфикс "i++" префиксом "++i" тогда все считается корректно.

Ну отвечают на это все - "это undefined behavior по Стандарту" и баг закрывают.

Разумеется единица!
i увеличили после окончания операции (его сохранённое значение) и записали на место получившегося было 2007 после окончания операции.
Чего удивительного? Удивительно было бы ожидать увидеть 2007...
Вот тогда действительно следовало бы репу долго чесать...
А если ++i тогда действительно 2007.
Никакой мистики. Просто нужно хотя бы приблизительно понимать, как реально осуществляет операции скомпилированный код.

salex комментирует...

Добрый день.
я правильно понял, что из логических операций точками следования являются только || и &&, а все остальные нет?

Alena комментирует...

Flint
я правильно понял, что из логических операций точками следования являются только || и &&, а все остальные нет?

Угу, я не вижу в Стандарте упоминаний про точки следования в других логических операциях.

Анонимный комментирует...

Забавно другое: что вопросы вида
i++ + ++i (написанные i+++++i) или что значит ++i++ (тут, правда, уже и другое примешивается) часто задавали на собеседованиях. При этом услышать хотели вовсе не "неопределенное поведение" (и тем более не точки следования), а именно про префиксный и постфиксный инкременты, порядок следования и т.д.

Когда вам на собеседовании задают этот вопрос, слышать хотят, конечно, не "неопределённое поведение" и даже не про порядок следования, а про то, что это семантическая ошибка. (++i) - не l-value, поэтому ни складывать не инкрементировать его дальше нельзя.

Анонимный комментирует...

Zefick
>> (++i) - не l-value, поэтому ни складывать не инкрементировать его дальше нельзя.

Я так понимаю речь про ++i++. Если да, то тогда уже не (++i) являеться помехой а (i++), так как приоритет у него больше... Кстати на MS С++ (++i) самое настоящее l-value. Поэтому такой вот код будет работать:
(++i)++
Чего нельзя сказать про C#. Там это не скомпилируеться.

Анонимный комментирует...

Ах да :) Забыл поблагодарить за статью, она ответила на многие мои вопросы. Спасибо огромное!!! И конечно же с восьмым марта! Которое, правда, уже час как девятое :)

Alena комментирует...

2VinSmile:

Ах да :) Забыл поблагодарить за статью, она ответила на многие мои вопросы. Спасибо огромное!!! И конечно же с восьмым марта!

Спасибо :-)

Которое, правда, уже час как девятое :)

Да ничего страшного :-)

Анонимный комментирует...

Что-то я запутался =)

Вот есть f(g(), h());
По Стандарту неизвестно g или h будет вызвана первой.
Предположим, g.
Вызов функции - точка следования.
В точке следования состояние реальной программы полностью соответствует состоянию абстрактной машины, описанной в Стандарте.
Получается, Стандартом определено, что первой будет вызвана g.

Alena комментирует...

2Анонимный:


Вот есть f(g(), h());
По Стандарту неизвестно g или h будет вызвана первой.
Предположим, g.
Вызов функции - точка следования.
В точке следования состояние реальной программы полностью соответствует состоянию абстрактной машины, описанной в Стандарте.
Получается, Стандартом определено, что первой будет вызвана g.


В начале было сделано странное предположение "Предположим, g." Потом на его основе делаются выводы, что неправильно.

Mishgan комментирует...

#include
int main(){
int i=0;
i = 6+ i++ + 2000;
std::cout << i << std::endl;
return 0;
}


MSVS 2008 дает результат 2007 8-)

Анонимный комментирует...

Автор +

Анонимный комментирует...

Никакой мистики. Просто нужно хотя бы приблизительно понимать, как реально осуществляет операции скомпилированный код.
Вот именно. Понимать надо всегда без исключения и прежде, чем давать подобные комментарии, нужно 200 раз подумать и передумать их писать.

Evg комментирует...

Возможно, об этом уже писалось, но я не осилил столько комментариев прочесть, а потому сделаю небольшое уточнение.

В виде некоторого итога писалось следующее:

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

Далее приводится пример на UB:

> x[i]=++i

В нём переменная i модифицируется всего один раз (т.е. мы не попадаем в указанное выше ограничение) и тем не менее остаётся UB. Можно вообще написать проще:

j = i + i++;

Здесь гораздо более очевидный UB, потому как при перестановке слагаемых сумма может поменяться.

Поэтому предлагаю скорректировать формулировку. Добавить (или даже заменить) что-то типа "если между двумя точками следования есть запись в переменную, то переменная между этими точками может быть использована только один раз, в противном случае мы придём к UB"

С уважением, Evg.

Alena комментирует...

Evg
Здесь гораздо более очевидный UB, потому как при перестановке слагаемых сумма может поменяться.

Поэтому предлагаю скорректировать формулировку.


Полная корректная формулировка есть в начале - "все побочные эффекты кода...", далее разбирается частный случай про две модификации.

Evg комментирует...

> Полная корректная формулировка есть в
> начале - "все побочные эффекты
> кода...", далее разбирается частный
> случай про две модификации.

Я так понимаю, что статья в первую очередь направлена на "новичков". На мой взгляд 9 из 10 начинающих из этой фразы НЕ сделают вывод в той формулировке, о которой я писал выше. А вывод сделают именно из той фразы, где говорится про две записи в переменную.

Я не навязываю свои идеи. В инете слишком мало статей, в которых действительно человеческим языком (а не для галочки) разжёваны неочевидные для начинающих (и не только начинающих) моменты. Твоя статья - одна из них, а потому не могу равнодушно пройти мимо. Просто хотелось бы более чёткой формулировки, сохраняя при этом её неформальность. И в обязательном порядке сохраняя понятность для того, кто видит статью в первый раз

Анонимный комментирует...

int a=0;
printf("%d",a,a++,a++,a++,a++,a++,a++);

относится ли данный код к UB?

Alena комментирует...

Анонимный
int a=0;
printf("%d",a,a++,a++,a++,a++,a++,a++);


Да, аналогично "примеру с функциями"

Waterfall комментирует...

Вообще говоря эти побочные эффекты - стрёмная штука. Функциональные языки в этом плане проще =)

Анонимный комментирует...

2 Waterfall: проще-то они проще, но там свои заморочки... не даром в них таки вводят присваивания и т.п. (правда, стараются хоть как-то формализовать это дело... у Пирса в книжке "Типы в языках программирования" вроде хорошо описано)
Ну и не для любой задачи хороши, иначе б давно вытеснили императивные.

Waterfall комментирует...

да, все верно. всему свое применение

vaychick комментирует...

Mishgan
int i=0;
i = 6+ i++ + 2000;
результат 2007 (MinGW в Qt 4.7.0 под Win)

Анонимный
Вот именно. Понимать надо всегда без исключения и прежде, чем давать подобные комментарии, нужно 200 раз подумать и передумать их писать.

Я подумал и согласен с автором, читая статью мне тоже сразу показалось,
что i++ после всех вычислений взял старое значение 0 и после этого инкрементировав его
затер 2006, может я конечно не прав но выглядит логично

Хотя на моей системе видать произошло другое, значение 2006 затерло старое значение 0 и потом его и инкрементировал post-инкремент.

Анонимный комментирует...

как насчёт выражений внутри if или while после них есть точка следования? Можно ли гарантировать при вхождении в блок все сайд-эффекты завершены?

Sergey D комментирует...

Недавно столкнулся с подобным вопросом на собеседовании. Пришлось достаточно упорно доказывать, почему в выражении "i += ++i + i + i++;" имеет место быть неопределенное поведение. Видимо, опять же, собеседующий надеялся услышать какой-то конкретный ответ.

Сергей комментирует...

Пожалуйста объясните мне правильно ли я понял:
1)получается что и = и i++, хоть и находятся в таблице предпочтений, но фактически занимают там "незаконные" места т.к. реально заканчивают своё выполнение точно не известно когда?
2)Или таблица предпочтений операторов (я тогда работал только под MSVS) это всегда нечто компайлер-специфик?
3) Почему в случае с ++i неопределённость пропадает, ведь если не было точки следования, где гарантия что запись увеличенного значения произойдёт сразу а не потом?

Alena комментирует...

Сергей

1)получается что и = и i++, хоть и находятся в таблице предпочтений, но фактически занимают там "незаконные" места т.к. реально заканчивают своё выполнение точно не известно когда?

Нет, не так. Порядок выполнения операций - это одно. Попытка изменить одну и ту же переменную дважды в одном выражении - это другое.
Принцип тут простой - не надо менять переменную в одном и том же выражении несколько раз. Тогда все встает на свои места, можно положиться на порядок выполнения.

2)Или таблица предпочтений операторов (я тогда работал только под MSVS) это всегда нечто компайлер-специфик?

Не специфик, порядок выполнения операций определен в Стандарте.

3) Почему в случае с ++i неопределённость пропадает, ведь если не было точки следования, где гарантия что запись увеличенного значения произойдёт сразу а не потом?

В смысле, в случае с ++i; ? Точка следования есть на точке с запятой. В этот момент всё фиксируется.

Анонимный комментирует...

Undefined behavior - это что? Компилятор может вместо этого назначить код очистки диска C:, если ему захочится?

Alena комментирует...

Анонимный

Undefined behavior - это что? Компилятор может вместо этого назначить код очистки диска C:, если ему захочится?

Вот тут у меня про это было подробно
http://alenacpp.blogspot.com/2005/08/unspecified-behavior-undefined.html

Анонимный комментирует...

2)Или таблица предпочтений операторов (я тогда работал только под MSVS) это всегда нечто компайлер-специфик?

Приоритет и порядок выполнения задает КАКОЙ ОПЕРАТОР БУДЕТ ИСПОЛЬЗОВАТЬ РЕЗУЛЬТАТ КАКОГО ОПЕРАТОРА.

Но о побочных эффектах выполнения оператора (т.е. то, что делает опрерато кроме вычисления результата. Пример
a=10; c=(a++ +1); //у того что в скобках побочный эффект увеличение a,
результ равен 11 )

ПРИМЕР:
A + ++B * C
Приоритет задает только то, что РЕЗУЛЬТАТ (++B) умножится на C
и РЕЗУЛЬТАТ умножения сложится с A

Но стандарт допускает, что побочный эффект (изменение переменной B) произойдет хоть в самом начале, хоть в конце хоть в середине.

Анонимный комментирует...

Правильно ли я понимаю, что вызов перегруженного оператора содержит точку следования?
В таком случае, если x -- переменная некоторого пользовательского класса с соответствующими перегруженными операторами, то примеры типа
(++x + ++x) + ++x
уже не undefined, а unspecified. Т.е. непонятно в каком порядке будут вычисляться операнды, но понятно, что после каждого вычисления побочные эффекты фиксируются. А если к тому же оператор + коммутативен, ассоциативен и не имеет побочных эффектов (вызов + не сказывается на результате ++x), то результат и вовсе однозначно определен. Или я ошибаюсь?

GrayFace комментирует...

С a=b=c=0 все однозначно занулятся. Нельзя сказать только, в каком порядке они занулятся. В том числе в ссылке https://groups.google.com/forum/#!topic/comp.lang.c++/VJhvpgWG554 это расписано.
"The result of the
assignment operation is the value stored in the left operand after the assignment has taken place; the result is an lvalue." - значит, значение, которое было до присвоения, ну никак не может быть возвращено.

Еще яснее в стандарте: http://www.rsdn.ru/forum/cpp/2321838.1
: это либо a = 0; b = 0;, либо b = 0; a = 0;. Аналогично с a=b=c=0.
"The result of the
assignment operation is the value stored in the left operand after the assignment has taken place; the result is an lvalue"

Разницу между ними можно почуствовать только с объеком, переопределяющим оператор присваивания.

Анонимный комментирует...

I've written a blog post about this topic, here: http://blog.szulak.org/dev/sequence-points-c/
Would you mind and leave your feedback, please?

regards,
szulak

Анонимный комментирует...

Отсюда вывод >>
в операции присваивания Переменной НЕ изменяй эту же Переменную
x += 2; // верно
x = x++ + 1; // НЕ верно

Анонимный комментирует...

Статья древняя. Баг в самом gcc

Анатолий комментирует...

Да это бред... Всё работет идеально!
Вот к примеру
x[i]=++i;
Ну что тут может быть неолпределённого?
Вначале вычисляется правая часть, т.е ++i, потом правая... новое i подставляется в индекс.
А замем присваевается x[i] это значение. Какая нах неопределённость? В Си нет никакой неопределённости и быть не может... И в стандарте этого нет. А есть чёткий порядок... Неверите, посмотрите Г Шитдт С 4-ое издание стр 76. Это стандарт! А если вы ссылаетесь на какой-то несуществующий стандарт, то это вши проблемы! В Си всё всегда определеоно... Это там разработчики Си сума сошли ибо код который десятилетиями компииролвался без сучка и задоринки вдруг стал неправильным. Это очень странное предупреждение появилось в gcc начиная с 5-ой версии. Просто заблокируйте его и не парьтесь
Я например не стану писать step++; step%=15; вместо прсотого и понятного step=++step%15;
Ну хотя бы потому что так удобнее. Оба кода работают одинаково. А если нет разницы зачем генерить старнные предупреждения! Считаю введение этих предупреждений большой глупостью...
А объяснеия неуместными. Если вы пытаетесь такое объяснить значит вы ничего не понимаете в Си...
Думаю эта дурь пройдёт и в следующих версиях комипилятора его уберут. Второе, если компиляторы выдают разные резулбьтаты на один и тот же код значит один из них работает неправильно.
Это бага не разработчика кода, а разработчика компилятора(одного из них) И неча ваши проблемы перекланывать на нашу шею, что якобы мы что-то неправильно написали. Пишите нормальный компилятор.. а то возьмём другой!

Анатолий комментирует...

f(g(), h()); Если бы вы лучше знали Си вы бы знали что и как вызывается....
Вначале берётся самый правый аргумент функции если там выражение или функция, то она вычислеятся, потом она загоняется в стек(Ну или в регистр, если у вас протокол ABI посмотрите там какой по счёту) потом берётся предпоследний и так далее, пока не будет передан первый аргумен, потом адресс возврата загоняется в стек адресс следующей за вызовом строки и с помощью goto(эти 2 комманды называются call) переход на первую строчку кода. Там вполняется фрейм входа...(см описание вызова функции) в конце возврат(выталкивается из стека адрес возврата) и переход к следующей за строкой вызова и дальше выполняется код... И так было всегда.. В си всегда параметры передавались справа на лево и стек очищала вызывающая функция. Именно блягодаря этому в Си есть функции с переменным числом аргументов. В иных языках аргументы передаютс(это же так же и порядок вчисления ибо перед тем как передать аргумент его надо вычислить) наоборот слева направо... И там невозможно организовать функции с переменным числом аргументов. Потому чт ов 64 битных процессорах которых сейчас 90% вызов регистровый по ABI. Это напрочь сбивает всю технологию вызова функций с переменным чсилом аргументов. Но она там иммитируется. Это приводит к большому количеству кода и данных и тормозит. Вобщем фигня получилась. Было бы хорошо отменить протокол ABI и делать стандартный стековый вызов было бы хорошо! Если кто знает аттрибуты функций подскажите... Я не нашёл.

Анатолий комментирует...

GarryFace пишет "a=b=c=0 все однозначно занулятся. Нельзя сказать только, в каком порядке они занулятся..." Вот это да! Ему неизвестно... цепочечные равенства все(как и обычные) всегда право ассоциативные. Это ещё одна фишка которую надо знать хорошо, чтобы правильно понимать сложный код на Си... если написано lvalue=rvalue; то в каом поряде оно вычисляется? ответьте вначале на этот вопрос. Однако вы знаете, что вначале вычисляется парвое выражение, а потом определяется адресс или место левого и туда записывается вычисленное значение. Нельзя смотреть на эт овыраженеи как на равенство. В программировании нет такой операции. На ассемблере это операция пересыллки. = это фикция... Но разработчики языка видимо смтрят на него как на математичское равенство. Вот потому у них и появилось это "неопределенное поведение"... А фактически это они не понимают основ программирования. И где они таких набирают только! Ну а дальзе всё понятно. Надо только учсть что значение выражения c=0; есть c, которое присваивается b, а затем a. Т.е. справа налево всегда и однозначно... И никак наче! В том и прелесть Си, что там всё однозначно... И в С++ тоже самое, без разницы. ООП не восит никаких собенностей, кроме того что теперь все эти операции есть операторы, т.е фунции собственно. Язык вообще не м.б неоднозначным. Кроме случая неинициализированных или неправильно инициализированных данных... Но это вы сами виноваты. В любой ячейке всегда что-то есть даже если вы туда ничего не записали(росто эт сделал кт о-то другой) При старте компа вся память нулевая. Некоторые данные инициализируются по умолчнию, но большинство нет. В Си только значения статических переменных. В С++ можно написать конструкторы которые будут это делать. Есть ещё функции инициализации и деинициализмации которые можно задать с помощью аттрибутов gcc. Даже в разделяемых мождулях(dll в винде). Но если вы начали использовать значение до того как записали туда чего-то и это не автоинициализируемая перменная то результат будет так же непредсказуем. Вот это и называется
неопределенное поведение... собственно! А собственно не то о чём тут пишут... Есть ещё одно место где неопределён порядок. Это статические перменные в разных файлах... Но всегда можно написать так, чтобы от этого никаких побочных эффектов не было! Усё...

Анонимный комментирует...

Чтобы больше не делать пустых заявлений, что в си всё всегда определено, потрудитесь прочитать еще одну страничку упомянутой вами книги и на стр.77 найдете подраздел "Порядок вычислений". Он ясно говорит, что в выражении
X = f1() + f2()
порядок вызова f1 и f2 не определен Стандартом и программисту нужно быть осторожным.

Анонимный комментирует...

Кучу статей, в том числе и эту, прочитал, но все равно не могу понять. Почему, например, выражение i = ++i; может давать разные результаты? Ведь есть приоритет операций! Почему про него нигде не упоминают? Согласно приоритету сначала должно выполниться ++i и только потом i =. Или я не прав???

kosiak комментирует...

Анонимный Анонимный пишет... Почему, например, выражение i = ++i;
Если бы ты написал i= i++, то UB однозначно. Потому что хоть приоритет и говорит, что сначала выполнить инкремент, далее в оператор = подаётся возвращаемое значение постинкремента, а оно в свою очередь совсем не равно i

Илья комментирует...

`i=1, i++; // все в порядке (на операторе запятая находится точка следования)`
то есть, это выражение эквивалентно в итоге
`i=1; i=i++;`, а второе выражение - UB.

Иначе получается, что оператор запятая - лечащий оператор, который любое UB может превратить не в UB, если просто записать `1,` перед выражением

Andrey Tarasevich комментирует...

Данную статью можно с определенной натяжкой назвать актуальной для 2005 года, но не более того.

В языке С++ уже почти десять лет как не существует понятия "точки следования". Концепция "точки следования" не является адекватным средством описания упорядочения вычисления выражений в С++, поэтому от нее в языке С++ полностью отказались.

Не пытайтесь руководствоваться вышеизложенным в современном C++.

Формально выражаясь, отказ языка C++ от идеи "точки следования" был сделан в том числе для того, чтобы исправить некоторые дефекты стандарта C++98. Так как исправления дефектов имеют ретроспективную силу, т.е. относятся и к предыдущим стандартам языка. Именно по этой причине использованный здесь подход можно назвать "актуальным для 2005 года" лишь с натяжкой.

Unknown комментирует...

Алена, спасибо! Статья замечательная и нужная, как и весь ваш материал

Только одно предложение сформулировано как-то не совсем по-русски:

"Но если у меня возникают сомнения - имеет ли в данном месте присвоение одной переменной несколько раз между двумя точками следования, я просто переписываю этот кусок."

Наверное, вы хотели написать "... имеет ли место в данном месте присвоение ..."
Но, чтобы избежать тавтологии, убрали нужное для связки слово :-)

Еще раз спасибо!