Псевдонимы, эквивалентные эквивалентным типам с подписью и без знака

83
11

Стандарты C и С++ позволяют использовать однотипные и однонаправленные варианты одинакового целочисленного типа для псевдонимов. Например, unsigned int* и int* могут быть псевдонимом. Но это не вся история, потому что они явно имеют различный диапазон представляемых ценностей. У меня есть следующие предположения:


    Если unsigned int считывается через int*, значение должно находиться в диапазоне int или происходит переполнение целого числа, а поведение undefined. Правильно ли это?
    Если int читается через unsigned int*, отрицательные значения обтекают, как если бы они были отброшены на unsigned int. Правильно ли это?
    Если значение находится в пределах диапазона int и unsigned int, доступ к нему через указатель любого типа полностью определен и дает такое же значение. Правильно ли это?

Кроме того, что относительно совместимых, но не эквивалентных целых типов?


    В системах, где int и long имеют одинаковый диапазон, выравнивание и т.д., могут int* и long* псевдоним? (Я предполагаю, что нет.)
    Может char16_t* и uint_least16_t* псевдоним? Я подозреваю, что это отличается от C и С++. В C, char16_t является typedef для uint_least16_t (правильно?). В С++ char16_t - это собственный примитивный тип, совместимый с uint_least16_t. В отличие от C, С++, похоже, не имеет исключения, позволяя совместимым, но различным типам с псевдонимом.

спросил(а) 2020-03-12T19:06:27+03:00 8 месяцев, 3 недели назад
1
Решение
92

Если a unsigned int считывается через int*, значение должно быть в диапазоне int или происходит переполнение целых чисел, и поведение undefined. Правильно ли это?



Почему это будет undefined? нет целочисленного переполнения, поскольку преобразование или вычисление не выполняется. Мы берем объектное представление объекта unsigned int и видим его через int. Каким образом значение объекта unsigned int переносится на значение int, полностью определяется реализацией.


Если a int считывается через unsigned int*, отрицательные значения обертывают вокруг, как если бы они были брошены в неподписанный int. Правильно ли это?



Зависит от представления. Да, с двумя дополнениями и эквивалентными дополнениями. Однако не с подтвержденной величиной - литой от int до unsigned всегда определяется через конгруэнтность:


Если тип назначения unsigned, результирующим значением является наименьшее беззнаковое целое, совпадающее с целым числом источника (по модулю 2 n где n - количество бит, используемых для представления неподписанного типа). [Примечание: в представлении двойного дополнения это преобразование является концептуальным и нет изменений в битовой схеме (если нет усечения). - конечная нота]



А теперь рассмотрим


10000000 00000001  // -1 in signed magnitude for 16-bit int

Это, безусловно, будет 2 15 +1, если интерпретироваться как unsigned. Листинг даст 2 16 -1, хотя.

Если значение находится в пределах интервала int и unsigned int, доступ к ней через указатель любого типа полностью определен и дает такое же значение. Правильно ли это?



Опять же, с двумя дополнениями и эквивалентными дополнениями, да. С знаковой величиной мы можем иметь -0.


В системах, где int и long имеют одинаковый диапазон, выравнивание, и т.д., может int* и long* псевдоним? (Я предполагаю, что нет.)



Нет. Они являются независимыми типами.


Может ли char16_t* и uint_least16_t* псевдоним?



Технически нет, но это кажется ненужным ограничением стандарта.


Типы char16_t и char32_t обозначают разные типы с одинаковыми размер, подпись и выравнивание как uint_least16_t и uint_least32_t, соответственно, в <cstdint>, называемом базовым типы.



Таким образом, это должно быть практически возможно без каких-либо рисков (поскольку не должно быть никаких дополнений).

ответил(а) 2020-03-12T19:21:13.303742+03:00 8 месяцев, 3 недели назад
71

Если int читается через unsigned int*, отрицательные значения обтекают, как если бы они были отброшены до unsigned int. Правильно ли это?



Для системы, использующей два дополнения, преобразование типа-punning и signed-to-unsigned эквивалентно, например:


int n = ...;
unsigned u1 = (unsigned)n;
unsigned u2 = *(unsigned *)&n;

Здесь оба u1 и u2 имеют одинаковое значение. Это, безусловно, самая распространенная настройка (например, Gcc документирует это поведение для всех своих целей). Тем не менее, стандарт C также обращается к машинам, использующим их дополнение или знак-значение для представления целых чисел. В такой реализации (предполагая, что никакие биты заполнения и никакие представления ловушки), результат преобразования целочисленного значения и типа-punning может дать разные результаты. В качестве примера предположим, что величина sign-величина и n инициализируются -1:


int n = -1;                     /* 10000000 00000001 assuming 16-bit integers*/
unsigned u1 = (unsigned)n; /* 11111111 11111111
effectively 2 complement, UINT_MAX */
unsigned u2 = *(unsigned *)&n; /* 10000000 00000001
only reinterpreted, the value is now INT_MAX + 2u */

Преобразование в неподписанный тип означает добавление/вычитание одного больше максимального значения этого типа до тех пор, пока значение не окажется в диапазоне. Выделение преобразованного указателя просто переинтерпретирует бит-шаблон. Другими словами, преобразование в инициализации u1 является no-op на двух машинах с дополнением, но требует некоторых вычислений на других машинах.


Если unsigned int считывается через int*, значение должно находиться в пределах диапазона int или происходит переполнение целого числа, а поведение undefined. Правильно ли это?



Не совсем. Битовая диаграмма должна представлять допустимое значение в новом типе, неважно, представилось ли старое значение. Из C11 (n1570) [опущенные сноски]:


6.2.6.2 Целые типы

Для беззнаковых целочисленных типов, отличных от unsigned char, биты представления объекта должны быть разделены на две группы: биты значений и биты заполнения (их не должно быть ни одного из последних). Если есть биты значений N, каждый бит должен представлять различную мощность 2 между 1 и 2 N-1 , так что объекты этого типа должны быть способны отображать значения от 0 до 2 N -1, используя чисто двоичное представление; это должно быть известно как представление стоимости. Значения любых битов дополнений не определены.

Для знаковых целых типов биты представления объекта должны быть разделены на три группы: биты значений, биты заполнения и знаковый бит. Не должно быть никаких битов заполнения; signed char не должно иметь никаких битов заполнения. Должен быть ровно один знаковый бит. Каждый бит, который является битом значения, должен иметь то же значение, что и тот же бит в представлении объекта соответствующего неподписанного типа (если в подписанном типе есть значения M), а N в неподписанном типе, затем M≤N). Если знаковый бит равен нулю, он не должен влиять на результирующее значение. Если знаковый бит равен единице, значение должно быть изменено одним из следующих способов:

    соответствующее значение со знаком бит 0 отрицается (знак и величина); знаковый бит имеет значение -2 M (два дополнения); знаковый бит имеет значение -2 M -1 (дополнение к ним).

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



Например, a unsigned int может иметь биты значений, где соответствующий подписанный тип (int) имеет бит заполнения, что-то вроде unsigned u = ...; int n = *(int *)&u; может привести к представлению ловушки на такой системе (чтение которой равно undefined), но не наоборот.

Если значение находится в пределах диапазона int и unsigned int, доступ к нему через указатель любого типа полностью определен и дает такое же значение. Правильно ли это?



Я думаю, что стандарт позволит одному из типов иметь бит дополнений, который всегда игнорируется (таким образом, два разных битовых шаблона могут представлять одно и то же значение и этот бит может быть установлен при инициализации), но всегда -trap-if-set для другого типа. Однако эта свобода ограничена, по крайней мере, там же. p5:

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



В системах, где int и long имеют одинаковый диапазон, выравнивание и т.д., могут int* и long* псевдоним? (Я предполагаю, что нет.)



Конечно, они могут, если вы их не используете;) Но нет, на таких платформах недопустимо следующее:


int n = 42;
long l = *(long *)&n; // UB

Может char16_t* и uint_least16_t* псевдоним? Я подозреваю, что это отличается от C и С++. В C, char16_t является typedef для uint_least16_t (правильно?). В С++ char16_t - это собственный примитивный тип, совместимый с uint_least16_t. В отличие от C, С++, похоже, не имеет исключения, позволяя совместимым, но различным типам с псевдонимом.



Я не уверен в С++, но, по крайней мере, для C, char16_t является typedef, но не обязательно для uint_least16_t, он вполне может быть typedef некоторой специфичной для реализации __char16_t, определенного типа несовместимый с uint_least16_t (или любым другим типом).

ответил(а) 2020-03-12T19:06:27+03:00 8 месяцев, 3 недели назад
59

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


Изменить

О, я неправильно прочитал фразу "но не эквивалентные целые типы", но я сохраняю этот абзац для вашего интереса:


У вашего второго вопроса есть больше проблем. Многие машины могут читать только по правильно выровненным адресам, там данные должны лежать на кратность ширины типов. Если вы читаете int32 с не-4-делимого адреса (потому что вы наложили 2-байтовый указатель int), ваш CPU может упасть.


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


Вывод:

Не делай это. Вы написали сильно зависимый от платформы (компилятор, целевой компьютер, архитектура) код, который скрывает свои ошибки за кастами, которые подавляют любые предупреждения.

ответил(а) 2020-03-12T19:06:27+03:00 8 месяцев, 3 недели назад
42

Относительно ваших вопросов относительно unsigned int* и int*: если
значение в фактическом типе не соответствует типу, который вы читаете,
поведение undefined, просто потому, что стандарт пренебрегает определением
любое поведение в этом случае и в любое время, когда стандарт не может определить
поведение, поведение undefined. На практике вы почти всегда будете
получить значение (никаких сигналов или чего-либо еще), но значение будет варьироваться
в зависимости от машины: машина со знаковой величиной или 1
дополнение, например, приведет к разным значениям (в обоих направлениях)
из обычного 2 дополнения.


В остальном int и long являются разными типами, независимо от их
представления и int* и long* не могут быть псевдонимом. Точно так же, как вы
скажем, в С++, char16_t - это отдельный тип в С++, но typedef в
C (поэтому правила, касающиеся сглаживания, различны).

ответил(а) 2020-03-12T19:06:27+03:00 8 месяцев, 3 недели назад
Ваш ответ
Введите минимум 50 символов
Чтобы , пожалуйста,
Выберите тему жалобы:

Другая проблема