Маленькая C-функция из преисподней

    Недавно мой студент и я пытались понять одну тонкость в стандарте C. Самый простой способ прояснить подобные вопросы — это узнать, учли ли её разработчики компиляторов, то есть написать код и посмотреть, что с ним будут делать разные компиляторы.

    Я написал такую функцию:
    int foo (char x) {
      char y = x;
      return ++x > y;
    }
    

    Так как ++x увеличивает на 1 значение x, очевидно, что функция должна возвращать "1" для большинства значений x. Вопрос состоит в том, что она вернет для значения CHAR_MAX?

    Одно предположение состояло в том, что функция однозначно будет возвращать "0". Другое — в том, что значение ++x не определено на платформах, где char — знаковый тип. В завершение была написана тестовая программа, печатающая выводы для всех возможных вариантов x:
    int main (void) {
      int i;
      for (i=CHAR_MIN; i<=CHAR_MAX; i++) {
        printf ("%d ", foo(i));
        if ((i&31)==31) printf ("\n");
      }
      return 0;
    }
    

    Этот код работает только на платформах, где char меньше int, но я не видел платформ, где бы встречалось обратное.
    Первый тревожный звоночек прозвонил, когда я скомпилировал код, используя Clang:

    regehr@home:~$ clang -O foo.c -o foo
    regehr@home:~$ ./foo
    0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
    0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
    0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
    0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
    0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
    0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
    0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
    0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
    

    Совсем не айс — функция должна в большинстве случаев возвращать единицу. Потом я понял, что использую устаревшую версию Clang (2.7) и скачал последний снэпшот (rev 126534) (Статья написана 3-го марта 2011 года — прим. пер.):

    regehr@home:~$ clang -O0 overflow.c -o overflow
    regehr@home:~$ ./overflow
    1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
    1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
    1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
    1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
    1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
    1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
    1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
    1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0
    regehr@home:~$ clang -O1 overflow.c -o overflow
    regehr@home:~$ ./overflow
    1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
    1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
    1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
    1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
    1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
    1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
    1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
    1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
    

    Обратите внимание на последний знак. Результат изменился, когда мы сменили уровень оптимизации — нормально, если значение инкрементированного CHAR_MAX не определено, в противном случае это ошибка компилятора.

    Intel C compiler (12.0.2 for x86-64) повел себя похожим образом:

    [regehr@bethe ~]$ icc -O0 foo.c -o foo
    [regehr@bethe ~]$ ./foo
    1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
    1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
    1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
    1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
    1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
    1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
    1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
    1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0
    [regehr@bethe ~]$ icc -O foo.c -o foo
    [regehr@bethe ~]$ ./foo
    1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
    1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
    1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
    1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
    1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
    1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
    1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
    1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
    

    Последняя версия GCC (rev 170512 для x86) дает стабильный результат:

    regehr@home:~$ current-gcc -O0 foo.c -o foo
    regehr@home:~$ ./foo
    1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
    1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
    1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
    1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
    1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
    1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
    1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
    1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0
    regehr@home:~$ current-gcc -O2 foo.c -o foo
    regehr@home:~$ ./foo
    1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
    1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
    1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
    1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
    1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
    1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
    1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
    1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0
    

    Тем не менее, история меняется, если мы просим компилятор не заниматься подстановкой функций:

    regehr@home:~$ current-gcc -O2 -fno-inline foo.c -o foo
    regehr@home:~$ ./foo
    1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
    1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
    1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
    1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
    1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
    1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
    1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
    1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
    

    Затем я попробовал CompCert и ситуация приняла неожиданный поворот:

    regehr@home:~$ ccomp foo.c -o foo
    regehr@home:~$ ./foo
    1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
    1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
    1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
    1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0
    1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
    1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
    1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
    1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
    

    Это очень странно, потому что CompCert содержит проверенную версию неявных преобразований языка C — именно для их тестирования и была написана foo().

    Короче говоря, так как тип char уже типа int и когда x имеет тип «Знаковый char» и значение CHAR_MAX, поведение ++x четко определено обоими стандартами ANSI C и C99.
    1. Стандарт гласит: «Выражение ++E эквивалентно выражению (E+=1)».
    2. Стандарт гласит: «Использование составного оператора присваивания в форме E1 op= E2 отличается от обычного присваивания E1 = E1 op (E2) только тем, что переменная E1 вычисляется только один раз».
    3. Правая часть выражения присваивания "E1 op E2" относится к «обычным арифметическим преобразованиям».
    4. Обычные арифметические преобразования подразумевают, что оба операнда к оператору "+" должны быть преобразованы из типа "signed char" к типу "signed int" перед выполнением операции сложения.
    5. Так как int шире char, отсутствует вероятность арифметического переполнения.

    Таким образом, поведение четко определено и каждый корректный компилятор должен выдавать следующий результат:
    1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
    1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
    1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
    1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
    1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
    1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
    1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
    1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0
    

    Другими словами, чтобы вычислить результат ++x > y, когда обе переменные являются переменными типа "signed char" и имеют значение CHAR_MAX:
    1. Signed char 127 преобразуется в signed int 127;
    2. Signed int 127 увеличивается на единицу, превращаясь в signed int 128;
    3. Signed int 128 преобразуется в signed char -128 — это новое значение x и выражения ++x (Замечание: такой тип преобразования не определен стандартом, но определяется и должен быть чётко описан конкретной реализацией);
    4. Signed char -128 преобразуется в signed int -128;
    5. Сравнение signed int -128 > signed int 127 возвращает "0".

    Так что насчет того факта, что ни один из четырёх компиляторов, которыми я обычно пользуюсь, не показал корректных результатов?
    • В GCC баг известен
    • В LLVM/Clang баг не был известен, но был исправлен в течение суток
    • Компилятор Intel также содержит ошибку.
    • В CompCert нет ошибки. Тем не менее, есть проблема взаимодействия между его определением беззнакового char и знаковых значений CHAR_MIN и CHAR_MAX, описанных в файле limits.h на моей linux-машине. Верификация компилятора и всего его окружения (заголовочные файлы, библиотеки, ассемблеры, процедуры ввода-вывода и т.д.) является серьезной и открытой проблемой. Вот почему мы и пишем тесты.

    Оригинал статьи — blog.regehr.org/archives/482
    Share post

    Similar posts

    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More
    Ads

    Comments 32

      +72
      В CompCert нет ошибки там жопа ваще.
        +1
        Только и пришло на ум — «как страшно жить». Если у человека 4 компилятора показали ошибку, то как верить, скажем, ПО кардиоводителя, или ПО системы жизнеобеспечения космического корабля, особенно, если на борту люди?

        Впрочем, какая жизнь без риска? :)
          0
          Там не верят, а проверяют. Решение в многочисленных и разнообразных тестах, которые покрывают максимум ситуаций. Т.к. в некоторых случаях ошибка может произойти, но будет не значительна. Точно так же, как любое техническое устройство может дать аппаратный сбой.
          Собственно поэтому один из вариантов — берут показатели 3х вычислений и сравнивают.
            0
            Согласен. Да и мне, честно говоря, не приходилось с таким количеством разнообразий от разных компиляторов на одном коде сталкиваться. Повезло, наверное. :)

            Граничные условия — всегда область трактований и удивлений.
        +5
        > Обычные арифметические преобразования подразумевают, что оба операнда к оператору "+" должны быть преобразованы из типа «signed char» к типу «signed int» перед выполнением операции сложения.

        Не так.
        Если, к примеру, сложить 2 signed char'a то никаких преобразований не будет.
        В данном случае они есть только потому что выполняется согласование типов — меньший (signed char: x) кастуется к большему (signed int: 1).
        Константа 1 это signed int просто потому что нет постфикса (u/l/ll) и она вмещается в int.
        Порядок проверки вместимости — int, long int, long long int.
        То есть числовая константа никогда не может быть short или char, собственно поэтому и кастуется, а не из-за каких-то странных мыслей в голове автора.
          +2
          А если быть точнее, то не «кастуется», а «промоутиться». Отличия — см. в стандарте.
            +2
            Увы, в терминологии никогда не был силён, больше по практика.
              0
              Нет проблем.
              +1
              Глянул в стандарт, там используются термины — явное/неявное преобразования.
              Собственно явное это и есть type cast expressions, а неявное — это то, что делает сам компилятор, без команд пользователя.
              +2
              Вы удиветесь, но в стандарте чётко написано, что вся знаковая целочисленная арифметика выполняется с int-ами, если по размеру подходит, если не подходит, то с long int-ами. Если вы не заметили преобразования, или если оптимизатор его выкинул, то это ещё не означает, что его не было :)
                –1
                Действительно удивлюсь.
                Покажите пруф, что ли.
                Additive operators: 6.5.6, item 4 — «If both operands have arithmetic type, the usual arithmetic conversions are performed on
                them»
                arithmetic type: 6.2.5, item 18: "Integer and floating types are collectively called arithmetic types"
                Integer types: 6.2.5, item 17: «The type char, the signed and unsigned integer types, and the enumerated types are collectively called integer types»
                usual arithmetic conversions: 6.3.1.8, item 11: "флоаты... If both operands have the same type, then no further conversion is needed. условие выполнилось, дальше читать не нужно"

                В общем если по-русски, то char — это целочисленный тип, целочисленный тип это арифметический тип.
                Арифметические типы при сложении следуют общем правилам кастинга бинарных операторов. Первое же правило для целых чисел — если типы одинаковые, то всё ок, оставляем так.
                Где здесь принудительная конвертация я не увидел.
                Особенно если учесть ремарки о том, что неявная конвертация прописывается только в соответствующих главах про выражения. (6.5.xxx)
                  +3
                  Нет, это не так:

                  #include <stdio.h>
                  #include <typeinfo>
                  
                  int main () {
                    char x = 1;
                    int i = 1;
                    
                    printf("type:%s\n", typeid(x+x).name());
                    printf("type:%s\n", typeid(x).name());
                    printf("type:%s\n", typeid(i).name());
                    return 0;
                  }
                  

                  Запустите этот код у себя, компилятор вам скажет, что он думает о типах. В Стандарте это прописано в правилах о integral promotions
                    0
                    Действительно, упустил строку «Otherwise, the integer promotions are performed on both operands».
              +6
              Мне вот когда-то понравился коммент в файле limits.h компилятора LLVM прямо перед определением CHAR_MAX и прочих констант:

              /* Many system headers try to «help us out» by defining these. No really, we know how big each datatype is. */
                –1
                Ужас какой.

                Какие-то совершенно детские ошибки в компиляторах, которые и современные, и распространённые.
                  +4
                  на платформах, где char — знаковый тип

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

                  Так вот, если использовать -funsigned-char, то в GCC 4.4/4.5/4.6/4.7 и Clang месячной давности всё работает правильно с любыми оптимизациями (1 1 1… 1 1 0).
                    0
                    А при опции -fsigned-char результат разный в зависимости от опций оптимизации. А обязан быть одинаковым.

                    Это баг.
                      0
                      ++x — undefined behaviour, с чего бы результат должн быть одинаовый?
                        0
                        Ох, да. В Стандарте прописано, что только для беззнаковых целых гарантируется отсутствие реакции на переполнение (результат обязан усекаться).

                        Для знаковой арифметики — да, undefined behavior.
                          0
                          Запутали вы меня. Как выглядит обоснование, что ++x — это именно undefined behavior, а не implementation-defined behavior?
                            0
                            Это не undefined behavior, а Implementation-defined:
                            C99, 6.3.1.3:3
                            «Otherwise, the new type is signed and the value cannot be represented in it; either the
                            result is implementation-defined or an implementation-defined signal is raised.»
                              0
                              Да, именно. Этот «undefined behavior» — только для таких экзотических платформ, у которых (INT_MAX+1) вызывает реакцию, аналогичную делению на ноль.

                              Все остальные должны четко прописывать, что именно должно получаться в результате. У GCC же результат «плавает».

                              В багзилле этот баг живёт уже 4 (!) года.
                              0
                              Да, правильно, implementation defined. Но undefined behavior — это тоже вариант решения проблемы implementation defined. И gcc приняло такую позицию, видимо.
                              +2
                              int foo (signed char x) {
                                signed char y = x;
                                //x++;  // #1
                                x=x+1;  // #2
                                return x > y;
                              }
                              

                              Строки #1 и #2 должны быть эквивалентны для компилятора. Однако, они приводят к разным результатам у GCC.

                              Это баг.
                                0
                                Вот здесь баг, согласен. Так как инкремент и сравнение разделены точкой следования. А значит x должен был принять какое-то значение, которое в соотвествии с его типом не может быть больше SCHAR_MAX.
                                  0
                                  Совсем не должны они быть эквивалентны.
                                  А вот если поменяете строку #2 на:
                                  x = x + (signed char)1;
                                  тогда можно будет об этой эквивалентности говорить.
                                    0
                                    Почему? Откуда это следует?
                                      0
                                      потому что в случае
                                      signed char x = 'a';
                                      x = x + 1;
                                      

                                      в правой части сложение не двух signed char, а signed char и int, а это значит, что первый операнд неявно расширяется до int. При x++; такого не происходит.
                                        0
                                        Ещё раз повторю вопрос: откуда вы это взяли?

                                        Т.к. (как я это вижу в Стандарте) при x++ происходит следующее:
                                        1. вычисляется x + 1:
                                        1.1. «x» расширяется до int.
                                        1.2. вычисляется результат (int(x) + 1)
                                        2. Полученный результат (типа int) присваивается переменной «x»:
                                        2.1. Так как int «шире», чем «signed char», то происходит усечение до типа «signed char»
                                        2.2. Усечённый результат заносится в «x»

                                        Всё. Этот порядок следует из эквивалентности выражений x++ и x=x+1. Где в Стандарте упоминается обратное?
                                          0
                                          char+char=int, это из стандарта следует и это так. Так что указанное Вами приведение типа не влияет.
                              0
                              Если бы речь шла о embedded, то у вас даже не возникло бы такого вопроса.
                                +2
                                Есть пару статей, не связанных напрямую с описанной проблемой, но тоже интересно почитать: http://blog.llvm.org/2011/05/what-every-c-programmer-should-know.html

                                Only users with full accounts can post comments. Log in, please.