Недействительные правила продвижения по типу

Это сообщение предназначено для использования в качестве часто задаваемых вопросов о неявном целенаправленном продвижении по C, в частности неявное продвижение, вызванное обычными арифметическими преобразованиями и/или целыми рекламными акциями.

Пример 1)
Почему это дает странное, большое целое число, а не 255?

unsigned char x = 0;
unsigned char y = 1;
printf("%u\n", x - y); 

Пример 2)
Почему это дает "-1 больше 0"?

unsigned int a = 1;
signed int b = -2;
if(a + b > 0)
  puts("-1 is larger than 0");

Пример 3)
Почему изменение типа в приведенном выше примере на short устраняет проблему?

unsigned short a = 1;
signed short b = -2;
if(a + b > 0)
  puts("-1 is larger than 0"); // will not print

(Эти примеры были предназначены для 32- или 64-разрядного компьютера с коротким 16 бит.)

Ответ 1

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

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

К сожалению, правила неявного продвижения типов приносят гораздо больше вреда, чем пользы, вплоть до того, что они могут быть одним из самых больших недостатков языка Си. Эти правила часто даже не известны среднему программисту C и поэтому вызывают всевозможные очень тонкие ошибки.

Обычно вы видите сценарии, в которых программист говорит "просто приведите к типу x, и это сработает" - но они не знают почему. Или такие ошибки проявляют себя как редкое, прерывистое явление, проникающее из, казалось бы, простого и понятного кода. Неявное продвижение особенно проблематично в коде, выполняющем битовые манипуляции, так как большинство побитовых операторов в C имеют плохо определенное поведение при получении подписанного операнда.


Целочисленные типы и рейтинг конверсии

Целочисленные типы в C: char, short, int, long, long long и enum.
_Bool/bool также рассматривается как целочисленный тип, когда речь идет о продвижении по типу.

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

Каждый целочисленный тип имеет целочисленный рейтинг преобразования, определенный следующим образом:
- Никакие два целых типа со знаком не должны иметь одинаковый ранг, даже если они имеют одинаковое представление.
- Ранг целочисленного типа со знаком должен быть больше ранга целочисленного типа со знаком с меньшей точностью.
- Ранг long long int должен быть больше, чем ранг long int, который должен быть больше, чем ранг int, который должен быть больше, чем ранг short int, который должен быть больше, чем ранг signed char.
- Ранг любого целого типа без знака должен равняться рангу соответствующего целого типа со знаком, если таковой имеется.

- Ранг любого стандартного целочисленного типа должен быть больше, чем ранг любого расширенного целочисленного типа с такой же шириной.
- Ранг чар равняется званию подписанного и без знака.
- Ранг _Bool должен быть меньше, чем ранг всех других стандартных целочисленных типов.
- ранг любого перечислимого типа должен равняться рангу совместимого целочисленного типа (см. 6.7.2.2).

Здесь также сортируются типы из stdint.h с тем же рангом, что и любому типу, которому они соответствуют в данной системе. Например, int32_t имеет тот же ранг, что и int в 32-битной системе.

Кроме того, C11 6.3.1.1 определяет, какие типы рассматриваются как целочисленные типы (не формальный термин):

Следующее может использоваться в выражении везде, где int или unsigned int могут использоваться:

- Объект или выражение с целочисленным типом (кроме int или unsigned int), чей ранг целочисленного преобразования меньше или равен рангу int и unsigned int.

На практике этот несколько загадочный текст означает, что _Bool, char и short (а также int8_t, uint8_t и т.д.) Являются "типами малых целых чисел". Они рассматриваются особым образом и подлежат скрытому продвижению, как описано ниже.


Целочисленные акции

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

Формально правило гласит (C11 6.3.1.1):

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

Это означает, что все маленькие целочисленные типы, независимо от подписи, неявно преобразуются в (подпись) int при использовании в большинстве выражений.

Этот текст часто неправильно понимают как: "все малые целочисленные типы со знаком преобразуются в целое число со знаком, а все малые целочисленные типы без знака преобразуются в целое число без знака". Это неверно Часть без знака здесь означает только то, что если у нас есть, например, операнд unsigned short, а размер int имеет тот же размер, что и short в данной системе, то операнд unsigned short преобразуется в unsigned int. Как, впрочем, ничего особенного на самом деле не происходит. Но в случае, если short является меньшим типом, чем int, он всегда преобразуется в (подписанный) int, независимо от того, был ли короткий подписан или не подписан!

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

Это может звучать как глупость, но, к счастью, компилятору разрешено оптимизировать код. Например, выражение, содержащее два операнда unsigned char, получит операнды, повышенные до int, а операция будет выполнена как int. Но компилятору разрешено оптимизировать выражение, чтобы оно фактически выполнялось как 8-битная операция, как и следовало ожидать. Однако здесь возникает проблема: компилятору не разрешено оптимизировать неявное изменение подписи, вызванное целочисленным продвижением. Потому что компилятор не может определить, действительно ли программист намеренно полагается на неявное продвижение или он непреднамеренный.

Вот почему пример 1 в вопросе терпит неудачу. Оба беззнаковых операнда переводятся в тип int, операция выполняется в типе int, а результат x - y имеет тип int. Это означает, что мы получаем -1 вместо 255, что можно было ожидать. Компилятор может генерировать машинный код, который выполняет код с 8-битными инструкциями вместо int, но он может не оптимизировать изменение подписи. Это означает, что мы получаем отрицательный результат, который, в свою очередь, приводит к странному числу, когда вызывается printf("%u. Пример 1 можно исправить, приведя результат операции обратно к типу unsigned char.

За исключением нескольких особых случаев, таких как операторы ++ и sizeof, целочисленные преобразования применяются почти ко всем операциям в C, независимо от того, используются ли унарные, двоичные (или троичные) операторы.


Обычные арифметические преобразования

Всякий раз, когда двоичная операция (операция с 2 операндами) выполняется в C, оба операнда оператора должны быть одного типа. Следовательно, в случае, если операнды имеют разные типы, C обеспечивает неявное преобразование одного операнда в тип другого операнда. Правила того, как это делается, называются обычными художественными преобразованиями (иногда неофициально именуемыми "балансировкой"). Они указаны в C11 6.3.18:

(Думайте об этом правиле как о длинном вложенном утверждении if-else if, и его может быть легче прочитать :))

6.3.1.8 Обычные арифметические преобразования

Многие операторы, которые ожидают операнды арифметического типа, вызывают преобразования и дают результат печатает аналогичным образом. Цель состоит в том, чтобы определить общий реальный тип для операндов и результат. Для указанных операндов каждый операнд конвертируется без изменения типа домен, к типу, чей соответствующий действительный тип является общим действительным типом. Если не явно указано иное, общий реальный тип также является соответствующим реальным типом результат, тип которого домен является доменом типа операндов, если они одинаковы, и сложный в противном случае. Этот шаблон называется обычным арифметическим преобразованием:

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

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

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

По этой причине a + b в примере 2 дает странный результат. Оба операнда являются целыми числами и имеют по крайней мере ранг int, поэтому целочисленные повышения не применяются. Операнды не одного типа - a это unsigned int и b это signed int. Поэтому оператор b временно преобразуется в тип unsigned int. Во время этого преобразования он теряет информацию о знаке и в итоге становится большим значением.

Причина, по которой изменение типа на short в примере 3 решает проблему, заключается в том, что short является целочисленным типом малого размера. Это означает, что оба операнда являются целочисленными и переводятся в тип int со знаком. После целочисленного преобразования оба операнда имеют один и тот же тип (int), дальнейшее преобразование не требуется. И тогда операция может быть выполнена на подписанном типе, как и ожидалось.

Ответ 2

Согласно предыдущему посту, я хочу дать больше информации о каждом примере.

Пример 1)

int main(){
    unsigned char x = 0;
    unsigned char y = 1;
    printf("%u\n", x - y); 
    printf("%d\n", x - y);
}

Поскольку unsigned char меньше, чем int, мы применяем к ним целочисленное продвижение, тогда у нас есть (int) x- (int) y = (int) (-1) и unsigned int (-1) = 4294967295.

Вывод из приведенного выше кода: (так же, как мы ожидали)

4294967295
-1

Как это исправить?

Я попробовал то, что рекомендовал предыдущий пост, но это на самом деле не работает. Вот код, основанный на предыдущем посте:

измените один из них на неподписанный int

int main(){
    unsigned int x = 0;
    unsigned char y = 1;
    printf("%u\n", x - y); 
    printf("%d\n", x - y);
}

Поскольку x уже является целым числом без знака, мы применяем только целочисленное продвижение к y. Тогда мы получим (без знака int) x- (int) y. Поскольку они по-прежнему не имеют одинаковый тип, мы применяем обычные арифметические преобразования, мы получаем (без знака int) x- (без знака int) y = 4294967295.

Вывод из приведенного выше кода: (так же, как мы ожидали):

4294967295
-1

Точно так же следующий код получает тот же результат:

int main(){
    unsigned char x = 0;
    unsigned int y = 1;
    printf("%u\n", x - y); 
    printf("%d\n", x - y);
}

изменить оба из них без знака Int

int main(){
    unsigned int x = 0;
    unsigned int y = 1;
    printf("%u\n", x - y); 
    printf("%d\n", x - y);
}

Поскольку оба они являются беззнаковыми int, целочисленное продвижение не требуется. По обычной арифметической конверсии (имеют тот же тип), (без знака int) x- (без знака int) y = 4294967295.

Вывод из приведенного выше кода: (так же, как мы ожидали):

4294967295
-1

Один из возможных способов исправить код: (добавьте приведение типа в конце)

int main(){
    unsigned char x = 0;
    unsigned char y = 1;
    printf("%u\n", x - y); 
    printf("%d\n", x - y);
    unsigned char z = x-y;
    printf("%u\n", z);
}

Выход из вышеприведенного кода:

4294967295
-1
255

Пример 2)

int main(){
    unsigned int a = 1;
    signed int b = -2;
    if(a + b > 0)
        puts("-1 is larger than 0");
        printf("%u\n", a+b);
}

Поскольку оба они являются целыми числами, целочисленное продвижение не требуется. При обычном арифметическом преобразовании мы получаем (без знака int) a+ (без знака int) b = 1 + 4294967294 = 4294967295.

Вывод из приведенного выше кода: (так же, как мы ожидали)

-1 is larger than 0
4294967295

Как это исправить?

int main(){
    unsigned int a = 1;
    signed int b = -2;
    signed int c = a+b;
    if(c < 0)
        puts("-1 is smaller than 0");
        printf("%d\n", c);
}

Выход из вышеприведенного кода:

-1 is smaller than 0
-1

Пример 3)

int main(){
    unsigned short a = 1;
    signed short b = -2;
    if(a + b < 0)
        puts("-1 is smaller than 0");
        printf("%d\n", a+b);
}

Последний пример исправил проблему, поскольку a и b оба преобразованы в int из-за целочисленного продвижения.

Выход из вышеприведенного кода:

-1 is smaller than 0
-1

Если я перепутал некоторые понятия, пожалуйста, дайте мне знать. Благодаря ~