Были ли типы переменной ширины заменены фиксированными типами в современном C?

21

Сегодня я наткнулся на интересный момент в обзоре Code Review . В этом ответе @Veedrac рекомендовал заменить типы переменного размера (например, intи long) на типы фиксированного размера, такие как uint64_tи uint32_t. Цитата из комментариев этого ответа:

Размеры int и long (и, следовательно, значения, которые они могут содержать) зависят от платформы. С другой стороны, int32_t всегда имеет длину 32 бита. Использование int просто означает, что ваш код работает по-разному на разных платформах, что, как правило, не то, что вы хотите.

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

Я думаю, изначально задумывалось, что каждый тип, кроме int, будет наименьшей вещью, которая может обрабатывать числа разных размеров, и что int будет наиболее практичным размером "общего назначения", который может обрабатывать +/- 32767.

Что касается меня, я всегда использовал intи не очень беспокоюсь об альтернативах. Я всегда думал, что это самый шедевр с лучшими характеристиками, конец истории. Единственное место, где я думал, что фиксированная ширина будет полезна, - это кодирование данных для хранения или передачи по сети. Я редко видел типы фиксированной ширины в коде, написанном другими.

Я застрял в 70-х годах или на самом деле есть смысл использовать intв эпоху C99 и далее?

jacwah
источник
1
Часть людей просто подражает другим. Я полагаю, что большая часть кода с фиксированным битом была сделана бессовестно. Нет причин устанавливать размер, ни не. У меня есть код, созданный в основном для 16-битных платформ (MS-DOS и Xenix 80-х), который просто компилируется и запускается сегодня на любых 64-х, а также обладает преимуществами нового размера слова и адресации, просто компилируя его. То есть сериализация для экспорта / импорта данных является очень важной архитектурой, обеспечивающей ее переносимость.
Лучано
1
Связанный: stackoverflow.com/questions/24444356/…
dan04

Ответы:

7

Существует распространенный и опасный миф о том, что такие типы, как uint32_tизбавление программистов от необходимости беспокоиться о размере int. Хотя было бы полезно, если бы Комитет по стандартам определил способ объявления целых чисел с машинно-независимой семантикой, неподписанные типы, такие как uint32_tсемантика, слишком свободны, чтобы позволить писать код чистым и переносимым способом; Кроме того, подписанные типы, такие как int32имеют семантику, которая для многих приложений определена излишне жестко и, таким образом, исключает то, что в противном случае было бы полезным для оптимизации.

Рассмотрим, например:

uint32_t upow(uint32_t n, uint32_t exponent)
{
  while(exponent--)
    n*=n;
  return n;
}

int32_t spow(int32_t n, uint32_t exponent)
{
  while(exponent--)
    n*=n;
  return n;
}

На машинах, где intне может храниться 4294967295 или 18446744065119617025, первая функция будет определена для всех значений nи exponent, и ее поведение не будет зависеть от размера int; кроме того, стандарт не будет требовать, чтобы он приводил к другому поведению на машинах с любым размером int некоторых значений, nи exponent, тем не менее, заставит его вызывать неопределенное поведение на машинах, где 4294967295 представляется как значение, intа 18446744065119617025 - нет.

Вторая функция выдаст Undefined Behavior для некоторых значений nи exponentна машинах, где intне может храниться 4611686014132420609, но даст определенное поведение для всех значений nи exponentна всех машинах, где это возможно (спецификации int32_tподразумевают, что поведение обертки с двумя дополнениями на машинах, где она меньше чем int).

Исторически, даже если в стандарте ничего не говорилось о том, что компиляторы должны делать с intпереполнением upow, компиляторы неизменно давали бы такое же поведение, как если бы intони были достаточно большими, чтобы не переполняться. К сожалению, некоторые новые компиляторы могут стремиться «оптимизировать» программы, устраняя поведение, не предусмотренное Стандартом.

Supercat
источник
3
Любой, кто захочет реализовать вручную pow, помните, что этот код является лишь примером и не предназначен для exponent=0!
Марк Херд
1
Я думаю, что вы должны использовать префиксный оператор декремента, а не постфикс, в настоящее время он делает 1 дополнительное умножение, например exponent=1, приведет к умножению n на себя один раз, так как декремент выполняется после проверки, если приращение выполняется до проверки ( т.е. --exponent), умножение не будет выполнено и само n будет возвращено.
ALXGTV
2
@MarkHurd: функция имеет плохое имя, поскольку фактически она вычисляет N^(2^exponent), но вычисления формы N^(2^exponent)часто используются при вычислении функций возведения в степень, а возведение в степень mod-4294967296 полезно для таких вещей, как вычисление хеш-функции конкатенации двух строк, чьи хеши известны.
суперкат
1
@ALXGTV: функция должна была иллюстрировать что-то, что вычисляло что-то, связанное с мощностью. На самом деле он вычисляет N ^ (2 ^ экспонента), который является частью эффективного вычисления показателя N ^, и вполне может дать сбой, даже если N мало (повторное умножение uint32_tна 31 никогда не даст UB, но эффективное способ вычисления 31 ^ N влечет за собой вычисления 31 ^ (2 ^ N), которые будут.
Суперкат
Я не думаю, что это хороший аргумент. Цель состоит не в том, чтобы сделать функции, определенные для всех входов, разумными или нет; это должно быть в состоянии рассуждать о размерах и переполнении. int32_tиногда определение переполнения, а иногда нет, о чем вы, вероятно, упоминаете, кажется минимальным по сравнению с тем фактом, что в первую очередь это позволяет мне рассуждать о предотвращении переполнения. И если вы хотите определенного переполнения, скорее всего, вы хотите получить результат по модулю некоторого фиксированного значения - так что вы все равно используете типы фиксированной ширины.
Veedrac
4

Для значений, тесно связанных с указателями (и, следовательно, с объемом адресуемой памяти), таких как размеры буфера, индексы массивов и Windows ' lParam, имеет смысл иметь целочисленный тип с размером, зависящим от архитектуры. Таким образом, типы переменного размера все еще полезны. Именно поэтому мы имеем определения типов size_t, ptrdiff_t, intptr_tи т.д. Они должны быть определением типов , поскольку ни один из встроенных в C целого числа типов не должно быть указатель размера.

Таким образом, вопрос , действительно ли char, short, int, long, и long longпо - прежнему полезны.

IME, для программ на C и C ++ все еще характерно использование intдля большинства вещей. И большую часть времени (то есть, когда ваши числа находятся в диапазоне ± 32 767, и у вас нет строгих требований к производительности), это работает просто отлично.

Но что, если вам нужно работать с числами в диапазоне 17-32 бит (например, в больших городах)? Вы могли бы использовать int, но это было бы жестким кодом зависимости платформы. Если вы хотите строго придерживаться стандарта, вы можете использовать long, который гарантированно должен быть не менее 32 бит.

Проблема состоит в том, что стандарт C не определяет какого-либо максимального размера для целочисленного типа. Существуют реализации, в которых longиспользуется 64 бита, что удваивает использование памяти. И если эти longэлементы окажутся элементами массива с миллионами элементов, вы поразите память как сумасшедшие.

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

  • Ваш компилятор I16L32 дает вам 32-разрядный long, избегая проблем усеченияint
  • Ваш компилятор I32L64 предоставляет вам 32-разрядную версию int, избегая потери памяти 64-разрядной long.
  • Ваш компилятор I36L72 дает вам 36-битный int

ОТО, предположим, вам не нужны огромные числа или огромные массивы, но вам нужна скорость. И intможет быть достаточно большим на всех платформах, но это не обязательно самый быстрый тип: 64-битные системы обычно все еще имеют 32-битные int. Но вы можете использовать int_fast16_tи получить «быстрый» тип, является ли это int, longили long long.

Итак, есть практические варианты использования для типов из <stdint.h>. Стандартные целочисленные типы ничего не значат . Особенно longэто может быть 32 или 64 бита, а может быть или не достаточно большим, чтобы содержать указатель, в зависимости от прихоти авторов компилятора.

dan04
источник
Проблема с такими типами uint_least32_tзаключается в том, что их взаимодействия с другими типами определены еще слабее, чем у uint32_t. ИМХО, стандарт должен определять такие типы, как uwrap32_tи unum32_t, с семантикой, которую любой компилятор, определяющий тип uwrap32_t, должен продвигать как тип без знака по существу в тех же случаях, что и при продвижении, если бы он intбыл 32-разрядным, и любой компилятор, который определяет тип, unum32_tдолжен гарантировать, что основные арифметические продвижения всегда конвертируют его в тип со знаком, способный удерживать его значение.
суперкат
Кроме того, Стандарт может также определять типы, чье хранение и псевдонимы совместимы с intN_tи uintN_t, и чьи определенные поведения будут согласовываться с intN_tи uintN_t, но которые предоставят компиляторам некоторую свободу в случае, когда код присваивает значения за пределами своего диапазона [допуская семантику, аналогичную тем, которые были возможно, предназначено для uint_least32_t, но без неопределенности, например, добавит ли a uint_least16_tи int32_ta результат со знаком или со знаком.
суперкат