Изменение типов или переосмысление базовых битов из одного типа в другой печально известно своим непредсказуемым и/или непереносимым поведением.
Например:
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 У меня есть код октодерева, использующий каламбур, чтобы быстро заполнить все 8 узлов значениями и проверить, имеют ли все 8 узлов одинаковое значение за 3 шага вместо 8, и я могу представить аналогичные варианты использования в любое время, где вы захотите диадический union
, состоящий из 1 целого типа одного целого типа, 2 целочисленного типа вполовину большего размера, 4 целочисленного типа четверти размера и т. д. и хочет иметь доступ к любому из отдельных членов или более крупных членов, которые содержат меньшие члены.
Я не могу найти ничего, что говорило бы, что в C это недопустимо. Есть ли в стандарте какие-то конкретные параграфы, которые вас беспокоят больше, чем другие? Отложите: логические предположения аннулируются, если поведение не определено.
@EricPostpischil Я не совсем понимаю, что нужно уточнить. Я сказал «при условии, что определены типы stdint.h
», тем самым ограничив объем вопроса системами, которые его поддерживают. Под «переносимостью» и «гарантией по стандарту» я имею в виду, существует ли какой-либо способ, в некоторой степени, соответствующая реализация C99, которая поддерживает stdint.h
, может дать неожиданные результаты и не выполнить указанное утверждение.
@EricPostpischil Я думал, что «четко определенное» означает «вызывает одно и то же поведение повсюду», тогда как поведение, определенное реализацией, неопределенное или неуказанное поведение, является «плохо определенным», и их поведение варьируется в зависимости от реализации, выполнения и т. д. Я не уверен, что Вместо этого мне следовало бы использовать термин, обозначающий «везде дает одни и те же результаты».
@CPlus: Нет, «четко определено» означает, что существует полная недвусмысленная спецификация (не официальное определение, а мое понимание этого термина). Если я знаю, что a
производит 34 на компьютерах, выпущенных до 1987 года, и 57 на компьютерах, выпущенных после этого, это четко определено, хотя его использование не переносится между разными компьютерами. «Хорошо определено» — это качество определения, а не переносимость кода.
Я не вижу, чтобы стандарт требовал, чтобы типы <stdint.h>
использовали определенные биты для определенных значений, либо изолированно, либо в сочетании с другими типами, за исключением требования в C 2018 6.2.6.2 2, что биты значения в целочисленном типе со знаком соответствуют биты значения в соответствующем целочисленном типе без знака. Другими словами, uint16_t
может использовать бит 0 для обозначения значения 2^3, бит 1 для 2^7, бит 2 для 2^9 и т. д., а uint8_t
может быть совершенно другим.
Я мог бы сформулировать это так: «Есть ли обстоятельства, при которых каламбур приводит к одинаковому поведению во всех реализациях C, соответствующих стандарту?» (И я мог бы попытаться еще больше сузить «обстоятельства».)
Учитывая гибкость присвоения значений битов, о которой я упоминал выше, шаблоны с нулевым битом или с одним битом в двух типах могут гарантировать одинаковое поведение, но любой другой шаблон битов не гарантируется между двумя несоответствующими типами. . Существует гарантированное поведение для произвольных шаблонов между соответствующими целочисленными типами со знаком и без знака.
Возможный заголовок: «Для каких типов T и U и значений V переинтерпретация значения V типа T как типа U дает идентичный результат во всех реализациях C, соответствующих стандарту?» (Обратите внимание, что нет необходимости указывать, что результат определен или определен, поскольку в противном случае он не будет идентичен во всех соответствующих реализациях.) (Кроме того, здесь мы разрешаем T или U быть типами массива, принимая их значение быть упорядоченным типом значений элементов.) Прекрасное название для математиков, но я полагаю, что другим захочется более простое английское название. Но в теле вопрос можно было бы поставить и так.
@EricPostpischil Я чувствую, что это могло бы быть лучше в теле вопроса, как разъяснение обстоятельств, поскольку этот заголовок был бы немного длинным.
Однако мы можем считать !! (union { uint8_t a[2]; uint16_t b; }) { 0x18, 0x18 } .b
ответом. Другими словами, хотя мы не можем узнать значение члена b
из-за проблемы с битовым значением, мы знаем, что оно не равно нулю, поэтому все реализации будут выдавать 1 для выражения. Аналогично, количество бит в b
будет равно количеству бит в a
.
@EricPostpischil Я знал о порядке байтов, но никогда не осознавал, что порядок битов тоже может быть проблемой.
Термин «каламбур» означает переосмысление представления типа как другого типа. Если типы гарантированно имеют одинаковые или достаточно четко определенные представления, то каламбур типов может быть переносимым.
Это прямо подтверждено в 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. Однако это определение указывает, что биты являются последовательными и упорядоченными.
Зачем требовать, чтобы биты целых чисел со знаком имели те же значения, что и соответствующие биты в беззнаковых значениях, если это было бы избыточно? Скорее всего, чтобы убедиться, что размещение бита заполнения и/или знака не «смещает» биты значения относительно соответствующего знакового типа.
Учитывая вышеизложенное, объединение идентичных копий фиксированного количества упорядоченных битов в новое фиксированное количество упорядоченных бит должно каждый раз давать одно и то же значение. Можно предположить, что любая реализация, которая не демонстрирует ожидаемого поведения в этом случае, нарушит определение чистой двоичной записи.
Теоретически это должно быть четко определено, но, как вы заявили, только для определенного набора значений. Существует ли реальный пример использования такой гарантии?