Каковы лучшие практики в отношении неподписанных целых?

43

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

Однако я замечаю из кода другого, что никто другой, кажется, не делает этого. Есть ли что-то важное, что я упускаю из виду?

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

wting
источник
26
Просто следите за for(unsigned int n = 10; n >= 0; n --)(петли бесконечно)
Крис Берт-Браун
3
В C и C ++ беззнаковые целочисленные типы точно определили поведение переполнения (по модулю 2 ^ n). Подписанные int не делают. Оптимизаторы все чаще используют это неопределенное поведение переполнения, что в некоторых случаях приводит к неожиданным результатам.
Steve314
2
Хороший вопрос! Я тоже когда-то испытывал желание использовать ограничения, ограничивающие диапазон, но обнаружил, что риск / неудобство перевешивают любую выгоду / удобство. Как вы сказали, большинство библиотек принимают обычные целочисленные значения там, где это делается. Это затрудняет работу, но также заставляет задуматься: стоит ли это того? На практике (при условии, что вы не делаете глупостей), вам редко будет приходиться значение -218, когда ожидается положительное значение. Это -218 должно быть откуда-то, верно? и вы можете проследить его происхождение. Бывает редко. Используйте утверждения, исключения, кодовые контракты, чтобы помочь вам.
Работа
@William Ting: если речь идет только о C / C ++, вы должны добавить соответствующие теги в свой вопрос.
CesarGon
2
@Chris: Насколько значительна проблема бесконечного цикла в реальности? Я имею в виду, что если он выйдет в релиз, то, очевидно, код не был протестирован. Даже если вам понадобится несколько часов для его отладки, когда вы в первый раз делаете эту ошибку, во второй раз вы должны знать, что искать в первую очередь, когда ваш код не прекращает цикл.
Безопасное

Ответы:

28

Есть ли что-то важное, что я упускаю из виду?

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

Я считаю, что это основная причина, по которой Java опускает типы unsigned int.

Майкл Боргвардт
источник
3
Другим решением было бы потребовать, чтобы вы вручную приводили свои числа соответствующим образом. Похоже, именно это и делает Go (я только немного поиграл с этим), и мне это нравится больше, чем подход Java.
Тихон Джелвис
2
Это было хорошей причиной для того, чтобы Java не включала 64-битный тип без знака, и, возможно, веская причина не включать 32-битный тип без знака [хотя семантика добавления 32-битных значений со знаком и без знака не была бы сложной - такая операция должна просто дать 64-битный результат со знаком]. Однако неподписанные типы, меньшие чем, intне представляют такой трудности (поскольку любые вычисления будут способствовать int); Я не могу сказать ничего хорошего об отсутствии типа без знака.
суперкат
17

Я думаю, что у Майкла есть верное замечание, но IMO причина, по которой все постоянно используют int (особенно in for (int i = 0; i < max, i++), заключается в том, что мы изучили его таким образом. Когда каждый пример в « как научиться программировать » книга использует intв forцикле, очень немногие когда - либо вопрос такой практики.

Другая причина в том, что intона на 25% короче uint, а мы все ленивые ... ;-)

треб
источник
2
Я согласен с образовательной проблемой. Большинство людей, кажется, никогда не задаются вопросом, что они читают: если это в книге, это не может быть неправильно, верно?
Матье М.
1
Вероятно, именно поэтому каждый использует постфикс ++при увеличении, несмотря на тот факт, что его конкретное поведение редко требуется и может даже привести к бессмысленному перетеканию копий, если индекс цикла является итератором или другим неосновным типом (или компилятор действительно плотный) ,
underscore_d
Только не делайте что-то вроде "for (uint i = 10; i> = 0; --i)". Использование только int для переменных цикла исключает эту возможность.
Дэвид Торнли
11

Кодирование информации о диапазоне в типы - это хорошо. Он обеспечивает использование разумных чисел во время компиляции.

Кажется, что во многих архитектурах есть специальные инструкции для работы с int-> floatпреобразованиями. Преобразование из unsignedможет быть медленнее (чуть-чуть) .

Бенджамин Банье
источник
8

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

Дэвид Шварц
источник
1
Возможно, лучше не смешивать допустимые значения с указанием ошибки в одной и той же переменной и использовать для этого отдельные переменные. Конечно, стандартная библиотека C здесь не является хорошим примером.
Безопасный
7

Для меня типы много о коммуникации. Используя явно беззнаковое целое, вы говорите мне, что подписанные значения не являются допустимыми значениями. Это позволяет мне добавить некоторую информацию при чтении вашего кода в дополнение к имени переменной. В идеале я неанонимный тип сказал бы мне больше, но он дает мне больше информации, чем если бы вы использовали целые числа везде.

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

daramarak
источник
4
Но я могу захотеть ограничить свои значения на месяц только от 1 до 12. Я использую другой тип для этого? Как насчет месяца? Некоторые языки фактически позволяют ограничивать такие значения. Другие, такие как .Net / C #, предоставляют кодовые контракты. Конечно, неотрицательные целые числа встречаются довольно часто, но большинство языков, которые поддерживают этот тип, не поддерживают дальнейшие ограничения. Итак, следует ли использовать сочетание uints и проверки ошибок, или просто сделать все через проверку ошибок? Большинство библиотек не запрашивают uint, где было бы целесообразно использовать один, следовательно, использование одного и приведение может быть неудобным.
Работа
@Job Я бы сказал, что вы должны использовать какие-то ограничения на использование компилятора / интерпретатора для ваших месяцев. Это может дать вам некоторый шаблон для настройки, но на будущее у вас есть принудительное ограничение, которое предотвращает ошибки и сообщает гораздо более четко, что вы ожидаете. Предотвращение ошибок и облегчение связи гораздо важнее, чем неудобства при реализации.
Дарамарак
1
«Я мог бы захотеть ограничить свои значения для месяца только 1–12». Если у вас есть конечный набор значений, таких как месяцы, вы должны использовать тип перечисления, а не необработанные целые числа.
Джош Касвелл
6

Я использую unsigned intв C ++ для индексов массивов, в основном, и для любого счетчика, который начинается с 0. Я думаю, что было бы хорошо сказать явно: «эта переменная не может быть отрицательной».

quant_dev
источник
14
Вы, вероятно, должны использовать size_t для этого в c ++
JohnB
2
Я знаю, я просто не могу быть обеспокоен.
quant_dev
3

Вам следует позаботиться об этом, когда вы имеете дело с целым числом, которое на самом деле может приближаться или превышать пределы подписанного типа int Поскольку положительный максимум 32-разрядного целого числа составляет 2 147 483 647, вам следует использовать целое число без знака, если вы знаете, что оно a) никогда не будет отрицательным и b) может достигать 2 147 483 648. В большинстве случаев, включая ключи базы данных и счетчики, я никогда даже не подойду к этим видам чисел, поэтому я не беспокоюсь о том, волнуюсь ли, используется ли знаковый бит для числового значения или для обозначения знака.

Я бы сказал: используйте int, если вы не знаете, что вам нужен неподписанный int.

Джоэл Этертон
источник
2
При работе со значениями, которые могут достигать максимальных значений, вы должны начать проверять операции на целочисленные переполнения, независимо от знака. Эти проверки обычно проще для неподписанных типов, потому что большинство операций имеют четко определенные результаты без неопределенного и определенного реализацией поведения.
Безопасный
3

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

Если вы когда-нибудь выполняете какое-либо высоконадежное программирование в Ada, вы даже используете различные типы переменных, таких как расстояние в футах и ​​расстояние в метрах, и компилятор помечает его, если вы случайно назначаете одно другому. Это идеально подходит для программирования управляемой ракеты, но излишне (каламбур), если вы проверяете веб-форму. В любом случае нет ничего плохого, если это соответствует требованиям.

Карл Билефельдт
источник
2

Я склонен согласиться с доводами Джоэла Этертона, но прихожу к противоположному выводу. На мой взгляд, даже если вы знаете, что числа вряд ли когда-либо приблизятся к пределам типа со знаком , если вы знаете, что отрицательных чисел не произойдет, то есть очень мало причин использовать вариант типа со знаком.

По той же причине, по которой я в некоторых выбранных случаях использовал BIGINT(64-разрядное целое), а не INTEGER(32-разрядное целое) в таблицах SQL Server. Вероятность того, что данные достигнут 32-битного предела в течение любого разумного промежутка времени, ничтожна, но если это произойдет, последствия в некоторых ситуациях могут быть весьма разрушительными. Просто убедитесь, что вы правильно сопоставляете типы между языками, иначе у вас будет странная странность ...

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

CVn
источник
Если вы работаете с данными, извлеченными из системы SAP, я настоятельно рекомендую BIGINT для полей идентификаторов (таких как CustomerNumber, ArticleNumber и т. Д.). Пока никто не использует буквенно-цифровые строки в качестве идентификаторов, то есть ... вздох
Треб
1

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

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

Supercat
источник
1
«Я бы рекомендовал [...] обычно использовать подписанные типы». Хм, вы забыли упомянуть преимущества подписанных типов и дали только список того, когда использовать неподписанные типы. "странность" ? В то время как большинство неподписанных операций имеют четко определенное поведение и результаты, вы вводите неопределенное и определяемое реализацией поведение при использовании подписанных типов (переполнение, сдвиг битов, ...). У вас есть странное определение «странности» здесь.
Безопасное
1
@Secure: «странность», на которую я ссылаюсь, связана с семантикой операторов сравнения, особенно в операциях, в которых используются смешанные типы со знаком и без знака. Вы правы в том, что поведение подписанных типов не определено при использовании значений, достаточно больших для переполнения, но поведение неподписанных типов может быть удивительным даже при работе с относительно небольшими числами. Например, (-3) + (1u) больше -1. Кроме того, некоторые нормальные математические ассоциативные отношения, которые применяются к числам, не относятся к беззнаковым. Например, (ab)> c не означает (ac)> b.
суперкат
1
@Secure: Хотя верно и то, что нельзя всегда полагаться на такое ассоциативное поведение с «большими» числами со знаком, но поведение работает должным образом при работе с числами, которые «малы» относительно области целых чисел со знаком. В противоположность этому, вышеупомянутое отсутствие ассоциации является проблематичным со значениями без знака «2 3 1». Между прочим, тот факт, что подписанное поведение имеет неопределенное поведение при использовании вне границ, может позволить улучшить генерацию кода на некоторых платформах при использовании значений, меньших, чем собственный размер слова.
суперкат
1
Если бы эти комментарии были в вашем ответе в первую очередь, вместо рекомендации и «обзывания» без объяснения причин, я бы не прокомментировал это. ;) Хотя я все еще не согласен с "странностью" здесь, это просто определение типа. Используйте правильный инструмент для данной работы, и, конечно, знайте инструмент. Типы без знака являются неправильным инструментом, когда вам нужны +/- отношения. Есть причина, почему size_tне подписано и ptrdiff_tподписано.
Безопасный
1
@Secure: Если кто-то хочет представить последовательность битов, неподписанные типы хороши; Я думаю, что мы согласны там. А на некоторых небольших микро-картах беззнаковые типы могут быть более эффективными для числовых величин. Они также полезны в тех случаях, когда дельты представляют числовые величины, а фактические значения - нет (например, порядковые номера TCP). С другой стороны, всякий раз, когда вычитают беззнаковые значения, нужно беспокоиться о угловых случаях, даже если числа невелики; такая математика со знаковыми значениями только представляет угловые случаи, когда числа большие.
суперкат
1

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

  • Когда вы выполняете арифметику с вашими беззнаковыми короткими переменными и литералами (которые имеют тип int) или переменными типа int, это гарантирует, что переменная без знака всегда будет повышаться до int перед вычислением выражения, поскольку int всегда имеет более высокий ранг, чем short , Это позволяет избежать любого непредвиденного поведения, выполняющего арифметику со знаковыми и беззнаковыми типами, при условии, что результат выражения вписывается в знаковое целое.
  • В большинстве случаев переменные без знака, которые вы используете, не превысят максимального значения короткого байта без знака (65 535)

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

Например, недавно у меня был цикл for что-то вроде этого:

const unsigned short cuint = 5;
for(unsigned short i=0; i<10; ++i)
{
    if((i-2)%cuint == 0)
    {
       //Do something
    }
}

Литерал '2' имеет тип int. Если бы я был unsigned int вместо unsigned short, то в подвыражении (i-2) 2 было бы переведено в unsigned int (поскольку unsigned int имеет более высокий приоритет, чем sign int). Если i = 0, то подвыражение равно (0u-2u) = некоторое массовое значение из-за переполнения. Та же идея с i = 1. Однако, так как я - беззнаковое короткое слово, оно получает тот же тип, что и литерал '2', который подписан как int, и все работает нормально.

Для дополнительной безопасности: в редком случае, когда архитектура, которую вы реализуете, приводит к тому, что значение int равно 2 байтам, это может привести к тому, что оба операнда в арифметическом выражении будут переведены в unsigned int, если неподписанная короткая переменная не подходит в подписанный 2-байтовый int, последний из которых имеет максимальное значение 32 767 <65 535. (См. Https://stackoverflow.com/questions/17832815/c-implicit-conversion-signed-unsigned для получения дополнительной информации). Чтобы избежать этого, вы можете просто добавить static_assert в вашу программу следующим образом:

static_assert(sizeof(int) == 4, "int must be 4 bytes");

и он не скомпилируется на архитектурах, где int составляет 2 байта.

AdmiralAdama
источник