Существуют ли какие-либо конкретные типы или значения, для которых каламбур приводит к одинаковому поведению во всех реализациях C, соответствующих стандарту?

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

Например:

union {
    unsigned u;
    float f;
} c = {.u = 10};
float f = c.f;

Не портативный, это будет зависеть от представления float.


union {
    unsigned char c[2];
    unsigned short s;
} c = {.c = {1, 2}};
short s = c.s;

Непереносимо, это будет зависеть от значения CHAR_BIT и порядка байтов/порядка байтов в системе.


Однако будет ли что-либо из следующего иметь гарантированное стандартом/переносимое поведение при условии, что определены все типы <stdint.h>:

union {
    uint8_t b[2];
    uint16_t w;
} c = {.b = {0x18, 0x18}};
assert(c.w == 0x1818);

Или наоборот:

union {
    uint8_t b[2];
    uint16_t w;
} c = {.w = 0x1818};
assert(c.b[0] == 0x18 && c.b[1] == 0x18);

Или если я увеличу размер типов:

union {
    uint16_t w[2];
    uint32_t l;
} c = {.w = {0x1818, 0x1818}};
assert(c.l == 0x18181818);

В приведенных выше примерах порядок байтов не имеет значения, поскольку число является «циклическим» и имеет одинаковое представление в формате big/little-endian или в любом другом эзотерическом порядке байтов. Типы гарантированно имеют точно указанную ширину битов и не имеют представлений ловушек или битов заполнения.

По этим причинам нет никакой логической причины, по которой type-каламбур будет иметь непереносимое поведение или возвращать какое-либо значение, кроме тех, которые указаны в assert(), но дает ли стандарт C такую ​​же гарантию явно? Действительно ли эти примеры портативны?

Стандарт C гласит, что чтение неактивного члена union будет «переинтерпретировать» биты в новый тип, но приведет ли это к тому, что приведенные выше примеры будут иметь переносимое поведение? Или есть какой-то способ, каким-то странным образом технически соответствующая реализация C99 может скомпилироваться, но не дать ожидаемых результатов?

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

dbush 24.03.2024 00:18

@dbush У меня есть код октодерева, использующий каламбур, чтобы быстро заполнить все 8 узлов значениями и проверить, имеют ли все 8 узлов одинаковое значение за 3 шага вместо 8, и я могу представить аналогичные варианты использования в любое время, где вы захотите диадический union, состоящий из 1 целого типа одного целого типа, 2 целочисленного типа вполовину большего размера, 4 целочисленного типа четверти размера и т. д. и хочет иметь доступ к любому из отдельных членов или более крупных членов, которые содержат меньшие члены.

CPlus 24.03.2024 00:22

Я не могу найти ничего, что говорило бы, что в C это недопустимо. Есть ли в стандарте какие-то конкретные параграфы, которые вас беспокоят больше, чем другие? Отложите: логические предположения аннулируются, если поведение не определено.

Ted Lyngmo 24.03.2024 00:37

@EricPostpischil Я не совсем понимаю, что нужно уточнить. Я сказал «при условии, что определены типы stdint.h», тем самым ограничив объем вопроса системами, которые его поддерживают. Под «переносимостью» и «гарантией по стандарту» я имею в виду, существует ли какой-либо способ, в некоторой степени, соответствующая реализация C99, которая поддерживает stdint.h, может дать неожиданные результаты и не выполнить указанное утверждение.

CPlus 24.03.2024 00:44

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

CPlus 24.03.2024 00:46

@CPlus: Нет, «четко определено» означает, что существует полная недвусмысленная спецификация (не официальное определение, а мое понимание этого термина). Если я знаю, что a производит 34 на компьютерах, выпущенных до 1987 года, и 57 на компьютерах, выпущенных после этого, это четко определено, хотя его использование не переносится между разными компьютерами. «Хорошо определено» — это качество определения, а не переносимость кода.

Eric Postpischil 24.03.2024 00:49

Я не вижу, чтобы стандарт требовал, чтобы типы <stdint.h> использовали определенные биты для определенных значений, либо изолированно, либо в сочетании с другими типами, за исключением требования в C 2018 6.2.6.2 2, что биты значения в целочисленном типе со знаком соответствуют биты значения в соответствующем целочисленном типе без знака. Другими словами, uint16_t может использовать бит 0 для обозначения значения 2^3, бит 1 для 2^7, бит 2 для 2^9 и т. д., а uint8_t может быть совершенно другим.

Eric Postpischil 24.03.2024 00:55

Я мог бы сформулировать это так: «Есть ли обстоятельства, при которых каламбур приводит к одинаковому поведению во всех реализациях C, соответствующих стандарту?» (И я мог бы попытаться еще больше сузить «обстоятельства».)

Eric Postpischil 24.03.2024 00:57

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

Eric Postpischil 24.03.2024 00:58

Возможный заголовок: «Для каких типов T и U и значений V переинтерпретация значения V типа T как типа U дает идентичный результат во всех реализациях C, соответствующих стандарту?» (Обратите внимание, что нет необходимости указывать, что результат определен или определен, поскольку в противном случае он не будет идентичен во всех соответствующих реализациях.) (Кроме того, здесь мы разрешаем T или U быть типами массива, принимая их значение быть упорядоченным типом значений элементов.) Прекрасное название для математиков, но я полагаю, что другим захочется более простое английское название. Но в теле вопрос можно было бы поставить и так.

Eric Postpischil 24.03.2024 01:01

@EricPostpischil Я чувствую, что это могло бы быть лучше в теле вопроса, как разъяснение обстоятельств, поскольку этот заголовок был бы немного длинным.

CPlus 24.03.2024 01:03

Однако мы можем считать !! (union { uint8_t a[2]; uint16_t b; }) { 0x18, 0x18 } .b ответом. Другими словами, хотя мы не можем узнать значение члена b из-за проблемы с битовым значением, мы знаем, что оно не равно нулю, поэтому все реализации будут выдавать 1 для выражения. Аналогично, количество бит в b будет равно количеству бит в a.

Eric Postpischil 24.03.2024 01:05

@EricPostpischil Я знал о порядке байтов, но никогда не осознавал, что порядок битов тоже может быть проблемой.

CPlus 24.03.2024 01:10
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
4
13
134
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Термин «каламбур» означает переосмысление представления типа как другого типа. Если типы гарантированно имеют одинаковые или достаточно четко определенные представления, то каламбур типов может быть переносимым.

Это прямо подтверждено в 6.2.5, сноске 39 (выделено мной):

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

Целые числа

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

Это означает, что любое положительное unsigned значение типа, меньшее или равное максимальному положительному значению соответствующего signed типа, будет иметь то же значение при вводе типа, и наоборот, поскольку все соответствующие биты в представлении должны иметь одинаковое значение. влияние на конечную стоимость.

Это гарантировано явно:

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

Кроме того:

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

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

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

Целые числа фиксированной ширины (если определены)

Написание каламбура от intN_t до uintN_t для того же N будет эквивалентно добавлению 2^(N-1) к значению, если значение intN_t отрицательное. Обратное действие будет эквивалентно вычитанию 2^(N-1) из значения, если значение uintN_t больше максимального значения intN_t.

Имя typedef intN_t обозначает целочисленный тип со знаком шириной N, без битов заполнения и с представлением с дополнением до двух. Таким образом, int8_t обозначает целочисленный тип со знаком шириной ровно 8 бит.

Это требование гарантирует отсутствие битов заполнения, и, поскольку они имеют одинаковое общее количество бит, количество битов значения в intN_t должно быть на один меньше, чем количество битов значения в uintN_t.

должен быть ровно один знаковый бит.

А поскольку все 15 битов значения в intN_t должны иметь те же значения, что и соответствующие биты в представлении uintN_t, и это дополнение до двух требуется для всех типов фиксированной ширины, путем исключения знаковый бит в intN_t должен соответствовать значению бит со значением 2^N-1 в uintN_t. Таким образом, каламбур между ними должен иметь переносимое поведение, как указано выше.

Указатели

В 6.2.5:

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

Это означает, что можно безопасно вводить каламбур между void * и char *, или между любыми двумя указателями struct, или любыми двумя указателями union, или между любыми двумя указателями на совместимые (например, подписанные и беззнаковые версии одного и того же типа) типы. Хотя можно преобразовать любой тип указателя объекта в void * или char *, для этого потребуется явное приведение типов, а не каламбур.

Структуры

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

В 6.5.2.3:

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

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

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

Кроме того:

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

Это означает, что если у вас есть несколько отдельных типов структур, но все их первые члены относятся к совместимым типам и в одном и том же порядке, их соответствующие члены могут быть доступны по типу/доступу, если в области доступа полностью объявлен union и видно оба. Пример из стандарта C:

Ниже приведен действительный фрагмент:

union {
   struct {
       int alltypes;
   } n;
   struct {
       int type;
       int intnode;
   } ni;
   struct {
       int type;
       double doublenode;
   } nf;
} u;
u.nf.type = 1;
u.nf.doublenode = 3.14;
/* ... */
if (u.n.alltypes == 1)
   if (sin(u.nf.doublenode) == 0.0)
       /* ... */

Следующий фрагмент не является допустимым (поскольку тип объединения не виден внутри функции f):

struct t1 { int m; };
struct t2 { int m; };
int f(struct t1 *p1, struct t2 *p2)
{
   if (p1->m < 0)
   p2->m = -p2->m;
   return p1->m;
}
int g()
{
   union {
       struct t1 s1;
       struct t2 s2;
   } u;
   /* ... */
   return f(&u.s1, &u.s2);
}

Объединение массивов меньших типов фиксированной ширины и более крупных типов фиксированной ширины

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

Это означает, что при отсутствии битов заполнения (что характерно для типов фиксированной ширины), каламбур между двумя последовательными типами до одного, вдвое большего, гарантированно будет иметь эффект объединения битов их объектных представлений. Непрерывность подразумевает, что между необработанными байтами в памяти не может быть «мусора».

Для целочисленных типов без знака [...] объекты этого типа должны быть способны представление значений от 0 до 2^N − 1 с использованием чисто двоичного представления;

Но гарантирует ли чистая двоичная запись, что биты значений в представлении объекта все больше упорядочиваются по величине?

Чистая двоичная запись определяется как:

Позиционное представление целых чисел, в котором используются двоичные цифры 0 и 1, в котором значения, представленные последовательными битами, являются аддитивными, начинаются с 1 и умножаются на последовательные целые степени 2, за исключением, возможно, бита с наивысшей позицией. (Адаптировано из Американского национального словаря систем обработки информации.) Байт содержит биты CHAR_BIT, а значения типа unsigned char варьируются от 0 до 2CHAR_BIT – 1.

Здесь явно упоминается представление, последовательные биты и позиция. Это означает, что биты в чисто двоичной записи упорядочены от самого низкого к самому высокому. Если бы это было не так, и точная позиция бита в представлении была бы бессмысленной, и в определении не упоминалось бы положение или то, что биты являются последовательными, то каждый из битов значения существует и соответствует каждому степень 2 между 0 и N. Однако это определение указывает, что биты являются последовательными и упорядоченными.

Зачем требовать, чтобы биты целых чисел со знаком имели те же значения, что и соответствующие биты в беззнаковых значениях, если это было бы избыточно? Скорее всего, чтобы убедиться, что размещение бита заполнения и/или знака не «смещает» биты значения относительно соответствующего знакового типа.

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

Другие вопросы по теме