Использование целых чисел без знака в C и C ++

23

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

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

В то же время различные руководства по кодированию, которые я читаю (например, Google), не поощряют использование целочисленных типов без знака, и, насколько мне известно, ни Java, ни Scala не имеют целочисленных типов без знака.

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

zzz777
источник

Ответы:

31

Есть две школы мысли об этом, и ни одна из них никогда не согласится.

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

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

Саймон Б
источник
8
Смешанные арифметические проблемы со знаком и без знака обнаруживаются компилятором; просто держите вашу сборку без предупреждений (с достаточно высоким уровнем предупреждения). Кроме того, intкороче набирать :)
rucamzu
7
Признание: я со второй школой мысли, и хотя я понимаю соображения для неподписанных типов: этого intболее чем достаточно для индексов массива 99,99% раз. Арифметические проблемы со знаком и без знака встречаются гораздо чаще и, таким образом, имеют приоритет с точки зрения того, чего следует избегать. Да, компиляторы предупреждают вас об этом, но сколько предупреждений вы получаете при компиляции любого значительного проекта? Игнорирование предупреждений опасно и является плохой практикой, но в реальном мире ...
Элиас Ван Отегем
11
+1 к ответу. Предостережение : тупые мнения впереди : 1: Мой ответ на вторую мысль: я бы поспорил, что любой, кто получит неожиданные результаты из беззнаковых целочисленных типов в C, будет иметь неопределенное поведение (а не чисто академическое) в их программы нетривиального C , которые используют подписанные интегральные тип. Если вы недостаточно хорошо знаете C, чтобы думать, что неподписанные типы лучше использовать, я советую избегать C. 2: Существует ровно один правильный тип для индексов и размеров массивов в C, и это так size_t, если только нет особого случая. хорошая причина в противном случае.
mtraceur
5
Вы сталкиваетесь с неприятностями без смешанной подписи. Просто вычислите unsigned int минус unsigned int.
gnasher729
4
Не спорю с вами, Саймон, только с первой школой мысли, которая утверждает, что «есть некоторые концепции, которые по своей природе не имеют знака - такие как индексы массивов». в частности: "Существует точно один правильный тип для индексов массива ... в C" Чушь! , Мы, DSP, постоянно используем отрицательные индексы. особенно с четными или нечетными импульсными характеристиками, которые не являются причинными. и для математики. я во второй школе мысли, но я думаю , что это полезно иметь оба подписанных и неподписанных целые числа в C и C ++.
Роберт Бристоу-Джонсон
21

Прежде всего, руководству по кодированию Google C ++ не очень хорошо следовать: оно избегает таких вещей, как исключения, повышение и т. Д., Которые являются основными в современном C ++. Во-вторых, только то, что определенное руководство работает для компании X, не означает, что оно будет подходящим для вас. Я бы продолжил использовать неподписанные типы, так как они вам нужны.

Правильное практическое правило для C ++: предпочитайте, intесли у вас нет веских причин использовать что-то еще.

bstamour
источник
8
Это не то, что я имею в виду. Конструкторы предназначены для установления инвариантов, и, поскольку они не являются функциями, они не могут просто, return falseесли этот инвариант не установлен. Таким образом, вы можете либо разделить вещи и использовать функции init для своих объектов, либо вы можете создать a std::runtime_error, позволить разматыванию стека произойти, и позволить всем вашим объектам RAII автоматически очистить себя, и вы, разработчик, можете обработать исключение, где это удобно для Вы должны сделать это.
bstamour
5
Я не вижу, как тип приложения имеет значение. Каждый раз, когда вы вызываете конструктор объекта, вы устанавливаете инвариант с параметрами. Если этот инвариант не может быть выполнен, вам нужно сообщить об ошибке, иначе ваша программа не в хорошем состоянии. Поскольку конструкторы не могут возвращать флаг, выдается исключение. Пожалуйста, дайте веский аргумент о том, почему бизнес-приложение не выиграет от такого стиля кодирования.
bstamour
8
Я очень сомневаюсь, что половина всех программистов на C ++ не способны правильно использовать исключения. Но в любом случае, если вы считаете, что ваши коллеги не способны писать на современном C ++, тогда, конечно, держитесь подальше от современного C ++.
bstamour
6
@ zzz777 Не использовать исключения? Есть частные конструктор, которые обертка фабричных функций общественных улавливают исключения и делать то , что - возвращать nullptr? вернуть объект по умолчанию (что бы это ни значило)? Вы ничего не решили - вы просто спрятали проблему под ковер и надеетесь, что никто не узнает.
Mael
5
@ zzz777 Если вы все равно собираетесь разбить коробку, почему вас волнует, что это происходит из-за исключения или signal(6)? Если вы используете исключение, 50% разработчиков, которые знают, как с ними обращаться, могут написать хороший код, а остальное может перенести их сверстник.
IllusiveBrian
6

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

Рассмотрим использование стандартного size_t в качестве индекса массива:

for (size_t i = 0; i < n; ++i)
    // do something here;

ОК, совершенно нормально. Затем рассмотрим, что мы решили изменить направление цикла по некоторым причинам:

for (size_t i = n - 1; i >= 0; --i)
    // do something here;

И сейчас это не работает. Если бы мы использовали intв качестве итератора, не было бы никаких проблем. Я видел такую ​​ошибку дважды за последние два года. Однажды это случилось в производстве и было трудно отлаживать.

Еще одна причина для меня - раздражающие предупреждения, которые заставляют вас писать что-то подобное каждый раз :

int n = 123;  // for some reason n is signed
...
for (size_t i = 0; i < size_t(n); ++i)

Это мелочи, но они складываются. Я чувствую, что код чище, если везде используются только целые числа со знаком.

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

Когда я компилирую следующий фрагмент кода с VS2015 или GCC, я не вижу предупреждений с настройками предупреждений по умолчанию (даже с -Wall для GCC). Вы должны попросить -Wextra, чтобы получить предупреждение об этом в GCC. Это одна из причин, по которой вы всегда должны компилировать с Wall и Wextra (и использовать статический анализатор), но во многих реальных проектах люди этого не делают.

#include <vector>
#include <iostream>


void unsignedTest()
{
    std::vector<int> v{ 1, 2 };

    for (int i = v.size() - 1; i >= 0; --i)
        std::cout << v[i] << std::endl;

    for (size_t i = v.size() - 1; i >= 0; --i)
        std::cout << v[i] << std::endl;
}

int main()
{
    unsignedTest();
    return 0;
}
Алексей Петренко
источник
Вы можете сделать это еще более неправильно с подписанными типами ... А ваш пример кода настолько умопомрачительный и явно неправильный, что любой порядочный компилятор предупредит, если вы попросите предупреждения.
Дедупликатор
1
В прошлом я прибегал к таким ужасам, for (size_t i = n - 1; i < n; --i)чтобы заставить его работать правильно.
Саймон Б
2
Говоря о циклах for size_tв обратном порядке, есть руководство по кодированию в стилеfor (size_t revind = 0u; revind < n; ++revind) { size_t ind = n - 1u - revind; func(ind); }
rwong
2
@ rwong Омг, это ужасно. Почему бы просто не использовать int? :)
Алексей Петренко
1
@AlexeyPetrenko - обратите внимание, что ни текущие стандарты C, ни C ++ не гарантируют, что intон достаточно большой, чтобы содержать все допустимые значения size_t. В частности, intможет разрешать числа только до 2 ^ 15-1, и обычно это делает в системах, которые имеют пределы выделения памяти 2 ^ 16 (или в некоторых случаях даже выше). longможет быть безопаснее сделать ставку, хотя все еще не гарантированно работать. Только size_tгарантированно работает на всех платформах и во всех случаях.
Жюль
4
for (size_t i = v.size() - 1; i >= 0; --i)
   std::cout << v[i] << std::endl;

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

for (size_t i = v.size(); i-- > 0; )
    std::cout << v[i] << std::endl;

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

size_t i = v.size();
while (i > 0)
{
    --i;
    std::cout << v[i] << std::endl;
}

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

Я был бы счастлив, если бы курсы начинающих не только показывали правильный цикл для подписанных, но и для неподписанных типов. Это позволило бы избежать пары ошибок, которые ИМХО следует обвинить невольным разработчикам вместо того, чтобы обвинять неподписанный тип.

НТН

Дон педро
источник
1

Целые числа без знака существуют по причине.

Рассмотрим, например, обработку данных в виде отдельных байтов, например, в сетевом пакете или файловом буфере. Иногда вы можете встретить таких зверей, как 24-битные целые числа. Легко сдвигается по битам из трех 8-разрядных целых чисел без знака, не так просто с 8-разрядными целыми числами со знаком.

Или подумайте об алгоритмах, используя таблицы поиска символов. Если символ представляет собой 8-разрядное целое число без знака, вы можете индексировать таблицу поиска по значению символа. Однако что делать, если язык программирования не поддерживает целые числа без знака? Вы бы имели отрицательные индексы для массива. Ну, я думаю, вы могли бы использовать что-то подобное, charval + 128но это просто ужасно.

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

Затем рассмотрите порядковые номера TCP. Если вы пишете какой-либо код обработки TCP, вам наверняка захочется использовать целые числа без знака.

Иногда эффективность так важна, что вам действительно нужен этот дополнительный бит без знака. Рассмотрим, например, устройства IoT, которые поставляются миллионами. Многие программные ресурсы могут быть оправданы для микрооптимизации.

Я бы сказал, что компилятор с соответствующими предупреждениями может преодолеть оправдание, позволяющее избежать использования целочисленных типов без знака (арифметика со смешанными знаками, сравнения со смешанными знаками). Такие предупреждения обычно не включены по умолчанию, но смотрите, например, -Wextraили отдельно -Wsign-compare(автоматическое включение в C с помощью -Wextra, хотя я не думаю, что это автоматическое включение в C ++) и -Wsign-conversion.

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

juhist
источник
0

Есть много случаев, когда целые числа на самом деле не представляют числа, но, например, битовую маску, идентификатор и т. Д. В основном, случаи, когда добавление 1 к целому числу не дает никакого значимого результата. В этих случаях используйте unsigned.

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

В очень редком случае, когда индексы превышают 2 ^ 31, но не 2 ^ 32, вы не используете целые числа без знака, вы используете 64-битные целые числа.

Наконец, хорошая ловушка: в цикле "for (i = 0; i <n; ++ i) a [i] ...", если я 32-разрядный без знака, а память превышает 32-разрядные адреса, компилятор не может оптимизировать доступ к [i] путем увеличения указателя, потому что при i = 2 ^ 32 - 1 я оборачиваюсь. Даже когда n никогда не становится таким большим. Использование целых чисел со знаком позволяет избежать этого.

gnasher729
источник
-5

Наконец, я нашел действительно хороший ответ: «Поваренная книга безопасного программирования» Дж. Виега и М. Мессье ( http://shop.oreilly.com/product/9780596003944.do )

Проблемы безопасности с целыми числами со знаком:

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

Есть проблемы с подписанными <-> беззнаковыми преобразованиями, поэтому не рекомендуется использовать mix.

zzz777
источник
1
Почему это хороший ответ? Что такое рецепт 3.5? Что это говорит о целочисленном переполнении и т. Д.?
Болдрикк
По моему практическому опыту. Это очень хорошая книга с ценными советами по всем другим аспектам, которые я пробовал, и она довольно тверда в этой рекомендации. По сравнению с этим опасность целочисленных переполнений в массивах длиннее 4G кажется довольно слабой. Если мне придется иметь дело с такими большими массивами, у моей программы будет много тонкой настройки, чтобы избежать снижения производительности.
zzz777
1
дело не в том, хороша ли книга. Ваш ответ не дает никаких оснований для использования получателя, и не у всех будет копия книги, чтобы найти ее. Посмотрите на примеры того, как написать хороший ответ
Baldrickk
К вашему сведению, я только что узнал о другой причине использования целых чисел без знака: можно легко обнаружить превышение: youtube.com/…
zzz777