Сколько будет i++ + ++i?

Vladimir
Опубликовано в: C/C++

За Write-only Code нужно отрывать руки

Вопрос для собеседования на вакансию C/C++-программиста:

int i = 5, j = i++ + ++i; - чему равно i и j?

Ответ вида “За такое нужно руки отрывать”, не подходит, ибо автор вопроса считает, что знает правильный ответ — i=7, j=12. Но так ли это?

Автор приводит следующую аргументацию:

Данная конструкция вполне приемлемая, в результате будет i=7, j=12 ((5+1) + 6, затем i увеличивается).

На самом деле, 7 и 12 — это один из нескольких возможных вариантов. А всё потому, что перед тем, как задавать такие вопросы, нужно читать стандарт языка.

В стандарте языка программирования C есть понятие точек следования (по-буржуйски — sequence points). Если говорит русским языком, точки следования — это места, в которых гарантируется, что все побочные эффекты предыдущих вычислений уже проявились, а побочные эффекты последующих вычислений еще не проявились. Это очень важно для вычисления выражений, в которых конечный результат выражения зависит от порядка вычислений подвыражений. Добавление точек следования является одним из методов обеспечения гарантированного значения результата, потому что они ограничивают способы, которыми выражение может быть вычислено.

В C/C++ ни оператор сложения, ни операторы префиксного/постфиксного инкремента не являются точками следования. К тому же, стандарт говорит следующее:

Between the previous and next sequence point an object shall have its stored value modified at most once by the evaluation of an expression. Furthermore, the prior value shall be accessed only to determine the value to be stored

Иными словами, из двух операций i++ и ++i только одна приведёт к изменению i. И в общем случае результат вычисления неоднозначных выражений не определён. Мило.

Но на практике компилятор руководствуется приоритетом операций и здравым смыслом. В частности, если скормить gcc подобную программку, её ассемблерный вариант будет выглядеть примерно так (я его привёл в читабельный вид):

[-]
View Code ASM
        mov     DWORD PTR [i], 5
        add     DWORD PTR [i], 1
        mov     eax, DWORD PTR [i]
        add     eax, eax
        mov     DWORD PTR [j], eax
        add     DWORD PTR [i], 1

Иными словами, получилось так:

[-]
View Code C
int i, j, tmp;
i = 5;
i = i + 1;
tmp = i;
j = tmp + tmp;
i = i + 1;

В соответствии с приоритетом операций компилятор сначала вычисляет выражение ++i, потом складывает результат с самим собой (получает j) и увеличивает i на единицу (постфиксный инкремент).

Проблема здесь в том, что стандарт говорит, что до достижения точки следования i может измениться только один раз. Только это надо доказать :)

Есть очень красивое доказательство.

[-]
View Code C
#include <stdio.h>

int main(void)
{
    int i;
    scanf("%d", &i);
    int j = i++ + ++i;
    printf("%d %d\n", i, j);
    return 0;
}

Скомпилируем и запустим программу. В первом случае безо всяких оптимизаций, во втором — со включённой оптимизацией:

[-]
View Code Text
$ gcc test.c -O0 -o test && ./test
5
6 11
$ gcc test.c -O3 -o test && ./test
5
7 12

Что и требовалось доказать: при разном уровне оптимизации получим разный результат.

Теперь объявим переменную статической:

[-]
View Code C
#include <stdio.h>

int main(void)
{
    static int i=5;
    int j = i++ + ++i;
    printf("%d %d\n", i, j);
    return 0;
}

и скомпилируем/запустим оба варианта:

[-]
View Code Text
$ gcc test.c -O0 -o test && ./test
5
6 11
$ gcc test.c -O3 -o test && ./test
5
7 12

Опять при одинаковых входных данных получаем разный результат.

На закуску: уберём static, добавим volatile, скомпилируем и запустим:

[-]
View Code Text
$ gcc test.c -O0 -o test && ./test
5
6 11
$ gcc test.c -O3 -o test && ./test
5
6 11

Вывод: результат зависит от того, какие оптимизации компилятор может применить. Кому интересно :-) — можно поэкспериментировать с алиасингом и указателями. Так что такое вот выражение является ярким примером write-only кода, за который, как известно, нужно отрывать руки :-)

Добавить в закладки

Связанные записи

13
Апр
2009

Комментарии к статье «Сколько будет i++ + ++i?» (5)  »

  1. Надежда says:

    Уважаемый Владимир!
    Аналогичные Вашему вопросы я регулярно задаю своим студентам на контрольных модулях по С++. К сожалению, мало кто отвечает. Прилагаю реализацию на VC++ 2005. Все три варианта реализации дают один и тот же результат.
    Вопрос: какой версией gcc Вы пользовались?

    • Vladimir says:

      gcc (Ubuntu 4.3.3-5ubuntu4) 4.3.3

      Visual C++ (особенно 2005) не очень-то славится поддержкой стандарта.

      • Надежда says:

        Да, Владимир, это правда. Но сколько людей его используют! Поэтому нужно знать его сильные и слабые стороны. А gcc, конечно, ОЧЕНЬ продвинутый и развивающийся компилятор, но мои студенты, к сожалению, его практически не знают.

  2. Надежда says:

    А это результат gcc-3.4.5 (без оптимизации)
    Enter value static int i= 5
    i=7 j=12

    Enter value int i= 5
    i=7 j=12

    Enter value volatile int i= 5
    i=6 j=11

  3. Vladimir says:

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

Подписаться на RSS-ленту комментариев к статье «Сколько будет i++ + ++i?» Trackback URL: http://blog.sjinks.org.ua/c-cpp/537-how-much-is-the-fish/trackback/

Оставить комментарий к записи «Сколько будет i++ + ++i?»

Вы можете использовать данные тэги: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>

Оставляя комментарий, Вы выражаете своё согласие с Правилами комментирования.

Подписаться, не комментируя