Интересная характеристика языка C по сравнению с некоторыми другими языками состоит в том, что многие из его типов данных основаны на размере слова целевой архитектуры, а не указываются в абсолютных терминах. Хотя это позволяет использовать язык для написания кода на машинах, которые могут иметь трудности с определенными типами, это очень затрудняет разработку кода, который будет последовательно работать на разных архитектурах. Рассмотрим код:
uint16_t ffff16 = 0xFFFF;
int64_t who_knows = ffff16 * ffff16;
В архитектуре, где int
16 бит (все еще верно для многих небольших микроконтроллеров), этот код будет присваивать значение 1, используя четко определенное поведение. На машинах с int
64-битным значением было бы присвоено значение 4294836225, опять же с использованием четко определенного поведения. На компьютерах с int
32-битным значением, вероятно, будет присвоено значение -131071 (я не знаю, будет ли это поведение, определяемое реализацией, или неопределенное поведение). Несмотря на то, что в коде не используется ничего, кроме того, что номинально считается типом «фиксированного размера», стандарт требует, чтобы два различных типа используемых сегодня компиляторов давали два разных результата, а многие популярные сегодня компиляторы - треть.
Этот конкретный пример несколько надуманный, поскольку в реальном коде я не ожидал бы, что произведение двух 16-битных значений напрямую назначается 64-битному значению, но он был выбран в качестве краткого примера, чтобы показать три целочисленных значения. рекламные акции могут взаимодействовать с типами без знака предположительно фиксированного размера. В некоторых реальных ситуациях необходимо выполнить математику для неподписанных типов в соответствии с правилами математической целочисленной арифметики, в других случаях необходимо, чтобы ее выполняли в соответствии с правилами модульной арифметики, а в некоторых случаях она действительно не выполняется. не имеет значения. Большая часть реального кода для таких вещей, как контрольные суммы, полагается на uint32_t
арифметическое обертывание мод 2³² и на способность выполнять произвольныеuint16_t
арифметические и получить результаты, которые, как минимум, определены как точный мод 65536 (в отличие от запуска неопределенного поведения).
Несмотря на то, что эта ситуация явно показалась бы нежелательной (и станет еще более важной, поскольку 64-битная обработка становится нормой для многих целей), комитет по стандартам C, из того, что я наблюдал, предпочитает вводить языковые функции, которые уже используются в некоторых заметных продуктах. среды, а не изобретать их "с нуля". Существуют ли какие-либо заметные расширения языка C, которые позволили бы коду указывать не только то, как будет храниться тип, но и как он должен вести себя в сценариях, связанных с возможным продвижением? Я вижу по крайней мере три способа, которыми расширение компилятора может решить такие проблемы:
Добавляя директиву, которая инструктирует компилятор принудительно устанавливать определенные «фундаментальные» целочисленные типы определенного размера.
Добавив директиву, которая инструктирует компилятор оценивать различные сценарии продвижения, как если бы типы машин имели определенные размеры, независимо от фактических размеров типов в целевой архитектуре.
Допуская средства объявления типов с определенными характеристиками (например, объявляют, что тип должен вести себя как оберточное алгебраическое кольцо mod-65536, независимо от базового размера слова, и не должен быть неявно конвертируемым в другие типы; добавление a
wrap32
к aint
должно привести к результат типаwrap32
независимо от тогоint
, больше ли он 16 бит, при этом добавление одногоwrap32
к awrap16
должно быть недопустимым (поскольку ни один из них не может быть преобразован в другой).
Моим собственным предпочтением была бы третья альтернатива, поскольку она позволяла бы даже машинам с необычными размерами слов работать с большим количеством кода, который ожидает, что переменные будут «переноситься», как это было бы с размерами степени двойки; компилятору, возможно, придется добавить инструкции для маскирования битов, чтобы заставить тип вести себя надлежащим образом, но если для кода требуется тип, охватывающий мод 65536, лучше, чтобы компилятор генерировал такую маскировку на машинах, которые в ней нуждаются, чем загромождать исходный код этим или просто иметь такой код непригодным для использования на машинах, где такая маскировка будет необходима. Однако мне любопытно, существуют ли какие-либо распространенные расширения, которые бы обеспечивали переносимость с помощью любого из вышеперечисленных средств или с помощью некоторых средств, о которых я не задумывался.
Чтобы уточнить, что я ищу, есть несколько вещей; наиболее заметно:
Хотя существует много способов, с помощью которых код может быть написан так, чтобы обеспечить желаемую семантику (например, определение макросов для выполнения do math для операндов без знака определенного размера, чтобы получить результат, который явно либо переносит, либо нет) или, по крайней мере, предотвращает нежелательный семантика (например, условно-определить тип, который
wrap32_t
должен бытьuint32_t
в компиляторах, где auint32_t
не будет продвигаться, и понять, что лучше для кода, который требуетwrap32_t
сбоя компиляции на машинах, где этот тип будет продвигаться, чем запускать его и выдавать поддельное поведение), если есть какой-либо способ написания кода, который будет наиболее выгодно работать с будущими языковыми расширениями, то использование этого было бы лучше, чем разработка моего собственного подхода.У меня есть довольно солидные идеи о том, как можно расширить язык, чтобы решить многие проблемы целочисленного размера, позволяя коду генерировать идентичные семантики на машинах с разными размерами слов, но прежде чем я потрачу много времени на их написание, я бы хотел узнать, какие усилия в этом направлении уже предприняты.
Я никоим образом не желаю, чтобы меня осуждали как Комитет по стандартам C или проделанную ими работу; Тем не менее, я ожидаю, что через несколько лет возникнет необходимость в корректной работе кода на машинах, где «естественный» тип продвижения будет 32-битным, а также на тех, где он будет 64-битным. Я думаю, что с некоторыми скромными расширениями языка (более скромными, чем многие другие изменения между C99 и C14), можно было бы не только обеспечить чистый способ эффективного использования 64-битных архитектур, но в сделке также облегчить взаимодействие с машины "необычного размера слова", которые стандарт исторически склонял назад для поддержки [например, позволяя машинам с 12-битным char
кодом выполнять код, который ожидаетuint32_t
обернуть мод 2³²]. В зависимости от того, в каком направлении пойдут будущие расширения, я бы также ожидал, что будет возможно определить макросы, которые позволят написанному сегодня коду использоваться в современных компиляторах, где целочисленные типы по умолчанию ведут себя как «ожидаемые», но также могут использоваться в будущих компиляторах, где целочисленные типы по умолчанию ведут себя по-разному, но где могут обеспечить требуемое поведение.
int
, но он все еще проскальзывает. (Опять же, при условии, что мое понимание стандарта C является правильным.)
int
оно большеuint16_t
, будет повышено до операндов умножения,int
и умножение будет выполняться какint
умножение, а полученноеint
значение будет преобразовано вint64_t
для инициализацииwho_knows
.