пятница, 15 мая 2009 г.

Точки следования

Недавно опять перечитывал "Новые сложные задачи на C++", сокрушаясь о своем уровне знания основного средства заработка...
Наткнувшись на пример с i+++++i, вспомнил про давнишнюю задачу, которую давали на одном из собеседований (слава Богу - не мне, поскольку предмет дискуссии очень гнилой, и услышать хотят явно не про UB).
int i = 10;
i = i + i++;

Типа, нужно порассуждать, чему равно i в результате...

На самом деле, рассуждать не нужно, нужно не использовать код, основанный на побочных эффектах ;-)
Кстати, под MSVC 2003 это раскрывается так:
int i = 10;
004119A9 mov dword ptr [i],0Ah
i = i + i++;
004119B0 mov eax,dword ptr [i]
004119B3 add eax,dword ptr [i]
004119B6 mov dword ptr [i],eax
004119B9 mov ecx,dword ptr [i]
004119BC add ecx,1
004119BF mov dword ptr [i],ecx


Из чего точно видно, что результат будет 21, вот только я почти уверен, что в gcc может быть другой результат, Digital Mars - третий, а Comeau - четвертый ;-)

Собственно, в Стандарте на эту тему есть вполне определенное понятие "Sequence Point", которое и определяет дальнейшие разговоры...

Короткое гугление натолкнуло меня на совершенно замечательный пост (рекомендую вообще этот блог почитывать, девушка пишет много интересного).

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

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

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

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

- В конце каждого полного выражения(Глава Стандарта 1.9/16). Обычно они помечены точкой с запятой;
- В точке вызова функции (1.9/17). Но после вычисления всех аргументов. Это и для inline функций в том числе.
- При возвращении из функции. (1.9/17) Есть точка следования сразу после возврата функции, перед тем как любой другой код из вызвавшей функции начал выполняться.

- После первого выражения (здесь оно называется 'a') в следующих конструкциях(1.9/18): "a || b", "a && b", "a , b", "a ? b : c"

Вычисление здесь идет слева направо. То есть левое выражение (по имени 'a') вычисляется и все побочные эффекты от такого вычисления завершаются. Потом, если все значение всего выражения известно, правое выражение не вычисляется, иначе вычисляется.

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

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

Т.е., предположим, что мы написали класс - имитатор int. Заменив тип переменной, мы в том же выражении, что и раньше, получим (точнее, можем получить) совершенно другую семантику того же самого выражения.... Клааасссс....

По этому поводу на Google'вской Open Party осенью 2008 года в Питере (уж и не знаю, как меня, С++-ника, туда занесло) Joshua Bloch за время своего выступления повторил раз 30 мантру "Behaviour might not be astonishing". Говорил он это, естественно, про Runtime библиотеку Java и ее архитектурные "сюрпризы", но он прав 1000% - резкое изменение семантики при крохотном изменении окружения способно надолго поставить в ступор даже самых опытных и подготовленных разработчиков :-(

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