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

12

Интересная характеристика языка C по сравнению с некоторыми другими языками состоит в том, что многие из его типов данных основаны на размере слова целевой архитектуры, а не указываются в абсолютных терминах. Хотя это позволяет использовать язык для написания кода на машинах, которые могут иметь трудности с определенными типами, это очень затрудняет разработку кода, который будет последовательно работать на разных архитектурах. Рассмотрим код:

uint16_t ffff16 = 0xFFFF;
int64_t who_knows = ffff16 * ffff16;

В архитектуре, где int16 бит (все еще верно для многих небольших микроконтроллеров), этот код будет присваивать значение 1, используя четко определенное поведение. На машинах с int64-битным значением было бы присвоено значение 4294836225, опять же с использованием четко определенного поведения. На компьютерах с int32-битным значением, вероятно, будет присвоено значение -131071 (я не знаю, будет ли это поведение, определяемое реализацией, или неопределенное поведение). Несмотря на то, что в коде не используется ничего, кроме того, что номинально считается типом «фиксированного размера», стандарт требует, чтобы два различных типа используемых сегодня компиляторов давали два разных результата, а многие популярные сегодня компиляторы - треть.

Этот конкретный пример несколько надуманный, поскольку в реальном коде я не ожидал бы, что произведение двух 16-битных значений напрямую назначается 64-битному значению, но он был выбран в качестве краткого примера, чтобы показать три целочисленных значения. рекламные акции могут взаимодействовать с типами без знака предположительно фиксированного размера. В некоторых реальных ситуациях необходимо выполнить математику для неподписанных типов в соответствии с правилами математической целочисленной арифметики, в других случаях необходимо, чтобы ее выполняли в соответствии с правилами модульной арифметики, а в некоторых случаях она действительно не выполняется. не имеет значения. Большая часть реального кода для таких вещей, как контрольные суммы, полагается на uint32_tарифметическое обертывание мод 2³² и на способность выполнять произвольныеuint16_t арифметические и получить результаты, которые, как минимум, определены как точный мод 65536 (в отличие от запуска неопределенного поведения).

Несмотря на то, что эта ситуация явно показалась бы нежелательной (и станет еще более важной, поскольку 64-битная обработка становится нормой для многих целей), комитет по стандартам C, из того, что я наблюдал, предпочитает вводить языковые функции, которые уже используются в некоторых заметных продуктах. среды, а не изобретать их "с нуля". Существуют ли какие-либо заметные расширения языка C, которые позволили бы коду указывать не только то, как будет храниться тип, но и как он должен вести себя в сценариях, связанных с возможным продвижением? Я вижу по крайней мере три способа, которыми расширение компилятора может решить такие проблемы:

  1. Добавляя директиву, которая инструктирует компилятор принудительно устанавливать определенные «фундаментальные» целочисленные типы определенного размера.

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

  3. Допуская средства объявления типов с определенными характеристиками (например, объявляют, что тип должен вести себя как оберточное алгебраическое кольцо mod-65536, независимо от базового размера слова, и не должен быть неявно конвертируемым в другие типы; добавление a wrap32к a intдолжно привести к результат типа wrap32независимо от того int, больше ли он 16 бит, при этом добавление одного wrap32к a wrap16должно быть недопустимым (поскольку ни один из них не может быть преобразован в другой).

Моим собственным предпочтением была бы третья альтернатива, поскольку она позволяла бы даже машинам с необычными размерами слов работать с большим количеством кода, который ожидает, что переменные будут «переноситься», как это было бы с размерами степени двойки; компилятору, возможно, придется добавить инструкции для маскирования битов, чтобы заставить тип вести себя надлежащим образом, но если для кода требуется тип, охватывающий мод 65536, лучше, чтобы компилятор генерировал такую ​​маскировку на машинах, которые в ней нуждаются, чем загромождать исходный код этим или просто иметь такой код непригодным для использования на машинах, где такая маскировка будет необходима. Однако мне любопытно, существуют ли какие-либо распространенные расширения, которые бы обеспечивали переносимость с помощью любого из вышеперечисленных средств или с помощью некоторых средств, о которых я не задумывался.

Чтобы уточнить, что я ищу, есть несколько вещей; наиболее заметно:

  1. Хотя существует много способов, с помощью которых код может быть написан так, чтобы обеспечить желаемую семантику (например, определение макросов для выполнения do math для операндов без знака определенного размера, чтобы получить результат, который явно либо переносит, либо нет) или, по крайней мере, предотвращает нежелательный семантика (например, условно-определить тип, который wrap32_tдолжен быть uint32_tв компиляторах, где a uint32_tне будет продвигаться, и понять, что лучше для кода, который требует wrap32_tсбоя компиляции на машинах, где этот тип будет продвигаться, чем запускать его и выдавать поддельное поведение), если есть какой-либо способ написания кода, который будет наиболее выгодно работать с будущими языковыми расширениями, то использование этого было бы лучше, чем разработка моего собственного подхода.

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

Я никоим образом не желаю, чтобы меня осуждали как Комитет по стандартам C или проделанную ими работу; Тем не менее, я ожидаю, что через несколько лет возникнет необходимость в корректной работе кода на машинах, где «естественный» тип продвижения будет 32-битным, а также на тех, где он будет 64-битным. Я думаю, что с некоторыми скромными расширениями языка (более скромными, чем многие другие изменения между C99 и C14), можно было бы не только обеспечить чистый способ эффективного использования 64-битных архитектур, но в сделке также облегчить взаимодействие с машины "необычного размера слова", которые стандарт исторически склонял назад для поддержки [например, позволяя машинам с 12-битным charкодом выполнять код, который ожидаетuint32_tобернуть мод 2³²]. В зависимости от того, в каком направлении пойдут будущие расширения, я бы также ожидал, что будет возможно определить макросы, которые позволят написанному сегодня коду использоваться в современных компиляторах, где целочисленные типы по умолчанию ведут себя как «ожидаемые», но также могут использоваться в будущих компиляторах, где целочисленные типы по умолчанию ведут себя по-разному, но где могут обеспечить требуемое поведение.

Supercat
источник
4
@RobertHarvey Ты уверен? Как я понимаю, целочисленное продвижение , если intоно больше uint16_t, будет повышено до операндов умножения, intи умножение будет выполняться как intумножение, а полученное intзначение будет преобразовано в int64_tдля инициализации who_knows.
3
@RobertHarvey Как? В коде OP нет упоминания о нем int, но он все еще проскальзывает. (Опять же, при условии, что мое понимание стандарта C является правильным.)
2
@RobertHarvey Конечно, это звучит плохо, но если вы не можете указать на такой способ, вы ничего не делаете, говоря «нет, вы, должно быть, делаете что-то не так». Вопрос в том, как избежать целочисленного продвижения или обойти его эффекты!
3
@RobertHarvey: Одна из исторических целей Комитета по C стандартов в том , чтобы сделать возможным практически для любой машины , чтобы иметь «C компилятор», и есть правила , быть достаточно конкретной , что независимо развитые компиляторы для какой - либо конкретной целевой машины будет быть в основном взаимозаменяемыми. Это было осложнено тем фактом, что люди начали писать компиляторы C для многих машин до того, как были разработаны стандарты, и Комитет по стандартам не хотел запрещать компиляторам делать то, на что мог бы опираться существующий код . Некоторые довольно фундаментальные аспекты стандарта ...
суперкат
3
... таковы, как они есть, не потому, что кто-то пытался сформулировать ряд правил, которые " имели смысл", а потому, что Комитет пытался закрепить все то, что было независимо у составителей , написанных независимо, которые уже существовали . К сожалению, этот подход привел к появлению стандартов, которые одновременно слишком расплывчаты, чтобы позволить программистам указывать, что нужно делать, но слишком специфичны, чтобы компиляторы «просто делали это».
суперкат

Ответы:

4

Как типичное намерение кода, как это

uint16_t ffff16 = 0xFFFF;
int64_t who_knows = ffff16 * ffff16;

чтобы выполнить умножение в 64 бита (размер переменной, в которой хранится результат), обычным способом получить (независимый от платформы) правильный результат является приведение одного из операндов для принудительного 64-битного умножения:

uint16_t ffff16 = 0xFFFF;
int64_t i_know = (int64_t)ffff16 * ffff16;

Я никогда не сталкивался с расширениями C, которые делают этот процесс автоматическим.

Барт ван Инген Шенау
источник
1
Мой вопрос заключался не в том, как заставить правильную оценку одного конкретного арифметического выражения (в зависимости от того, какой результат требуется), либо в приведении операнда к uint32_tдругому, либо в использовании макроса, определенного как один #define UMUL1616to16(x,y)((uint16_t)((uint16_t)(x)*(uint16_t)(y)))или в #define UMUL1616to16(x,y)((uint16_t)((uint32_t)(x)*(uint16_t)(y)))зависимости от размера int), а скорее в том, существуют ли любые новые стандарты для того, как обрабатывать такие вещи с пользой, а не определять мои собственные макросы.
суперкат
Я должен был также упомянуть, что для таких вещей, как хеширование и вычисления контрольной суммы, часто целью является получение результата и усечение его до размера операндов. Типичным намерением выражения like (ushort1*ushort2) & 65535uбыло бы выполнение арифметики mod-65536 для всех значений операндов. Читая обоснование C89, я думаю, что довольно ясно, что, хотя авторы признавали, что такой код может не работать в некоторых реализациях, если результат превысил 2147483647, они ожидали, что такие реализации будут становиться все более редкими. Однако такой код иногда не работает на современном gcc.
суперкат