Почему результаты целостного продвижения отличаются?

Посмотрите мой тестовый код:

#include <stdlib.h>
#include <stdio.h>


#define PRINT_COMPARE_RESULT(a, b) \
    if (a > b) { \
        printf( #a " > " #b "\n"); \
    } \
    else if (a < b) { \
        printf( #a " < " #b "\n"); \
    } \
    else { \
        printf( #a " = " #b "\n" ); \
    }

int main()
{
    signed   int a = -1;
    unsigned int b = 2;
    signed   short c = -1;
    unsigned short d = 2;

    PRINT_COMPARE_RESULT(a,b);
    PRINT_COMPARE_RESULT(c,d);

    return 0;
}

В результате получается следующее:

a > b
c < d

Моя платформа - Linux, а моя версия gcc - 4.4.2. Меня удивляет вторая линия вывода. Первая строка вывода вызвана целым продвижением. Но почему результат второй линии отличается?

Следующие правила относятся к стандарту C99:

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

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

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

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

Я думаю, что оба этих сравнения должны принадлежать одному и тому же случаю, второй случай целочисленного продвижения.

Ответы

Ответ 1

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

Целочисленные акции: Если int может представлять все значения типа, то операнд продвигается до int. Это относится как к short, так и к unsigned short на большинстве платформ. Преобразование, выполняемое на этом этапе, выполняется на каждом операнде индивидуально, независимо от другого операнда. (Существует больше правил, но это применимо.)

Обычные арифметические преобразования: Если вы сравниваете unsigned int с a signed int, так как ни один из них не включает весь диапазон другого, и оба имеют одинаковый ранг, то оба они преобразуются в unsigned. Это преобразование выполняется после изучения типа обоих операндов.

Очевидно, что "обычные арифметические преобразования" не всегда применяются, если не существует двух операндов. Вот почему существуют два набора правил. Например, один из них - это то, что операторы сдвига << и >> не выполняют обычных арифметических преобразований, так как тип результата должен зависеть только от левого операнда (поэтому, если вы видите кого-то типа x << 5U, тогда U означает "ненужное" ).

Разбивка: Предположим, что типичная система с 32-битным int и 16-разрядным коротким.

int a = -1;         // "signed" is implied
unsigned b = 2;     // "int" is implied
if (a < b)
    puts("a < b");  // not printed
else
    puts("a >= b"); // printed
  • Сначала поддерживаются два операнда. Поскольку оба значения int или unsigned int, промо-акции не выполняются.
  • Далее, два операнда преобразуются в один и тот же тип. Поскольку int не может представлять все возможные значения unsigned, а unsigned не может представлять все возможные значения int, нет очевидного выбора. В этом случае оба преобразователя преобразуются в unsigned.
  • При преобразовании из подписанного в unsigned 2 32 повторно добавляется к значению, подписанному до тех пор, пока оно не окажется в диапазоне значений без знака. На самом деле это проблема с процессором.
  • Таким образом, сравнение становится if (4294967295u < 2u), что неверно.

Теперь попробуйте его с помощью short:

short c = -1;          // "signed" is implied
unsigned short d = 2;
if (c < d)
    puts("c < d");     // printed
else
    puts("c >= d");    // not printed
  • Во-первых, продвигаются два операнда. Так как оба они могут быть точно представлены int, оба повышаются до int.
  • Затем они преобразуются в один и тот же тип. Но они уже одного типа, int, поэтому ничего не сделано.
  • Таким образом, сравнение становится if (-1 < 2), что верно.

Написание хорошего кода: Там есть простой способ поймать эти "gotchas" в вашем коде. Просто всегда компиляция с включенными предупреждениями и исправление предупреждений. Я обычно пишу код следующим образом:

int x = ...;
unsigned y = ...;
if (x < 0 || (unsigned) x < y)
    ...;

Вы должны следить за тем, чтобы любой код, который вы пишете, не запускался в другой подписанный vs. unsigned gotcha: signed overflow. Например, следующий код:

int x = ..., y = ...;
if (x + 100 < y + 100)
    ...;
unsigned a = ..., b = ...;
if (a + 100 < b + 100)
    ...;

Некоторые популярные компиляторы оптимизируют (x + 100 < y + 100) до (x < y), но это история за другой день. Просто не переполняйте свои подписанные номера.

Сноска: обратите внимание, что если signed подразумевается для int, short, long и long long, это НЕ подразумевается для char. Вместо этого это зависит от платформы.

Ответ 2

Взято из стандарта С++:

4.5 Интегральные рекламные акции [conv.prom]
1 Значение r char, подписанное char, unsigned char, short int или unsigned short int может быть преобразуется в rvalue типа int, если int может представлять все значения Тип источника; в противном случае исходное значение r может быть преобразовано в rvalue типа unsigned int.

На практике это означает, что все операции (по типам в списке) фактически оцениваются по типу int, если он может охватывать весь набор значений, с которым вы имеете дело, в противном случае он выполняется на unsigned int, В первом случае значения сравниваются как unsigned int, потому что один из них был unsigned int, и поэтому -1 является "большим", чем 2. Во втором случае значения a, сравниваемые как целые числа со знаком, как int, покрывают весь домен как short, так и unsigned short и, следовательно, -1 меньше 2.

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

Ответ 3

Процесс преобразования для С++ описывается как обычные арифметические преобразования. Тем не менее, я думаю, что наиболее релевантное правило находится в разделе с ссылкой ниже conv.prom: Интегральные рекламные акции 4.6.1:

Значение целочисленного типа, отличного от bool, char16_t, char32_t или wchar_t, чей целочисленный ранг преобразования ([conv.rank]) меньше, чем rank int может быть преобразован в prvalue типа int, если int может представляют все значения типа источника; в противном случае источник prvalue может быть преобразовано в prvalue типа unsigned int.

Самое смешное, что есть слово "can", которое, я думаю, предполагает, что эта акция выполняется по усмотрению компилятора.

Я также нашел этот фрагмент C-spec, который подсказывает отсутствие пропуска:

11   EXAMPLE 2       In executing the fragment
              char c1, c2;
              /* ... */
              c1 = c1 + c2;
     the ``integer promotions'' require that the abstract machine promote the value of each variable to int size
     and then add the two ints and truncate the sum. Provided the addition of two chars can be done without
     overflow, or with overflow wrapping silently to produce the correct result, the actual execution need only
     produce the same result, possibly omitting the promotions.

Существует также определение определения "ранг" . Список правил довольно длинный, но поскольку он применяется к этому вопросу, "ранг" прост:

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