Является ли использование неподписанного, а не подписанного int более вероятным причиной ошибок? Зачем?

в Руководство по стилю Google C ++, по теме «Целые числа без знака» предлагается

Из-за исторической случайности стандарт C ++ также использует целые числа без знака для представления размера контейнеров — многие члены органа по стандартизации считают, что это ошибка, но на данный момент исправить это практически невозможно. Тот факт, что арифметика без знака не моделирует поведение простого целого числа, а вместо этого определяется стандартом для моделирования модульной арифметики (обтекание при переполнении / недостаточном заполнении), означает, что компилятор не может диагностировать значительный класс ошибок.

Что не так с модульной арифметикой? Разве это не ожидаемое поведение int без знака?

К каким ошибкам (значительный класс) относится руководство? Переполненные ошибки?

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

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

75

Решение

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

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

Беззнаковые значения имеют разрыв в нуле, наиболее распространенное значение в программировании

И целые числа без знака и со знаком имеют разрывы в их минимальных и максимальных значениях, где они переносятся (без знака) или вызывают неопределенное поведение (со знаком). За unsigned эти точки в нуль а также UINT_MAX, За int они в INT_MIN а также INT_MAX, Типичные значения INT_MIN а также INT_MAX в системе с 4 байтами int значения -2^31 а также 2^31-1и по такой системе UINT_MAX обычно 2^32-1,

Основная проблема с ошибками unsigned это не относится к int в том, что у него есть разрыв в нуле. Ноль, конечно, является очень распространенным значением в программах, наряду с другими небольшими значениями, такими как 1,2,3. Обычно складывают и вычитают небольшие значения, особенно 1, в различных конструкциях, а если вы вычитаете что-либо из unsigned значение, и оно оказывается равным нулю, вы только что получили огромное положительное значение и почти определенную ошибку.

Рассмотрим код, повторяющий все значения в векторе по индексу, кроме последнего0,5:

for (size_t i = 0; i < v.size() - 1; i++) { // do something }

Это работает нормально, пока однажды вы не передадите пустой вектор. Вместо того, чтобы делать ноль итераций, вы получаете v.size() - 1 == a giant number1 и вы сделаете 4 миллиарда итераций и почти будете иметь уязвимость переполнения буфера.

Вам нужно написать это так:

for (size_t i = 0; i + 1 < v.size(); i++) { // do something }

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

Существует аналогичная проблема с кодом, который пытается выполнить итерирование вплоть до нуля. Что-то вроде while (index-- > 0) работает нормально, но, видимо, эквивалент while (--index >= 0) никогда не завершится для значения без знака. Ваш компилятор может предупредить вас, когда правая часть буквальный ноль, но, конечно, нет, если это значение определяется во время выполнения.

контрапункт

Некоторые могут возразить, что подписанные значения также имеют две несплошности, так зачем выбирать неподписанные? Разница в том, что оба разрыва очень (максимально) далеки от нуля. Я действительно считаю, что это отдельная проблема «переполнения», при этом значения со знаком и без знака могут переполняться при очень больших значениях. Во многих случаях переполнение невозможно из-за ограничений на возможный диапазон значений, а переполнение многих 64-битных значений может быть физически невозможно). Даже если это возможно, вероятность ошибки, связанной с переполнением, часто ничтожна по сравнению с ошибкой «в ноль», и переполнение происходит и для неподписанных значений. Таким образом, unsigned сочетает в себе худшее из обоих миров: потенциальное переполнение с очень большими значениями величины и разрыв в нуле. Подписано только бывшее.

Многие будут утверждать, что «вы немного потеряете» с неподписанным. Это часто верно, но не всегда (если вам нужно представить разницу между значениями без знака, вы все равно потеряете этот бит: так много 32-битных вещей в любом случае ограничено 2 ГБ, или у вас будет странная серая область, где, скажем, файл может быть 4 ГиБ, но вы не можете использовать определенные API во второй половине 2 ГБ).

Даже в тех случаях, когда unsigned покупает вас немного: он мало что покупает: если вам нужно было поддерживать более 2 миллиардов «вещей», вам, вероятно, скоро придется поддерживать более 4 миллиардов.

Логически, неподписанные значения являются подмножеством подписанных значений.

Математически, беззнаковые значения (неотрицательные целые числа) являются подмножеством целых чисел со знаком (просто называемых _integers).2. Еще подписанный значения естественно всплывают из операций исключительно на неподписанный значения, такие как вычитание. Мы можем сказать, что неподписанные значения не закрыто под вычитанием. То же самое не относится к подписанным значениям.

Хотите найти «дельту» между двумя беззнаковыми индексами в файле? Что ж, вам лучше сделать вычитание в правильном порядке, иначе вы получите неправильный ответ. Конечно, вам часто требуется проверка во время выполнения, чтобы определить правильный порядок! Имея дело со значениями без знака в виде чисел, вы часто обнаруживаете, что (логически) значения со знаком продолжают появляться в любом случае, так что вы могли бы также начать со знака.

контрапункт

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

Правда, но диапазон менее полезен. Рассмотрим вычитание и числа без знака с диапазоном от 0 до 2N, а также числа со знаком с диапазоном от -N до N. Произвольные вычитания приводят к результатам в диапазоне от -2N до 2N в обоих случаях, и любое целое число может представлять только половина этого. Хорошо получается, что область вокруг нуля от -N до N обычно более полезна (содержит больше фактических результатов в коде реального мира), чем диапазон от 0 до 2N. Рассмотрим любое типичное распределение, отличное от равномерного (log, zipfian, normal и т. Д.), И рассмотрим вычитание случайно выбранных значений из этого распределения: гораздо больше значений заканчивается в [-N, N], чем [0, 2N] (действительно, в результате получается распределение всегда в центре нуля).

64-разрядная версия закрывает двери по многим причинам использовать подписанные значения в качестве чисел

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

За пределами специализированных доменов 64-битные значения в значительной степени устраняют эту проблему. 64-битные значения со знаком имеют верхний диапазон 9 223 372 036 854 775 807 — более девяти нониллион. Это много наносекунд (около 292 лет) и много денег. Это также больший массив, чем у любого компьютера, который может иметь ОЗУ в согласованном адресном пространстве в течение длительного времени. Так что, может быть, 9 квинтиллионов хватит всем (пока)?

Когда использовать неподписанные значения

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

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

Действительно, есть хорошие применения для беззнаковых переменных:

  • Когда вы хотите обрабатывать N-разрядное число не как целое число, а просто как «мешок с битами». Например, в качестве битовой маски или растрового изображения, или N логических значений или чего-либо еще. Это использование часто идет рука об руку с фиксированными типами ширины, такими как uint32_t а также uint64_t так как вы часто хотите знать точный размер переменной. Намек на то, что определенная переменная заслуживает такого обращения, заключается в том, что вы работаете с ней только с помощью побитовое такие операторы, как ~, |, &, ^, >> и так далее, а не с арифметическими операциями, такими как +, -, *, / и т.п.

    Без знака здесь идеально, потому что поведение побитовых операторов четко определено и стандартизировано. У значений со знаком есть несколько проблем, таких как неопределенное и неопределенное поведение при сдвиге и неопределенное представление.

  • Когда вы на самом деле хотите модульную арифметику. Иногда вы действительно хотите 2 ^ N модульной арифметики. В этих случаях «переполнение» — это функция, а не ошибка. Значения без знака дают вам то, что вы хотите, поскольку они определены для использования модульной арифметики. Подписанные значения нельзя (легко, эффективно) использовать вообще, поскольку они имеют неопределенное представление, а переполнение не определено.

0,5 После того, как я написал это, я понял, что это почти идентично Пример Джарода, которого я не видел — и для этого есть хороший пример!

1 Мы говорим о size_t здесь обычно 2 ^ 32-1 в 32-битной системе или 2 ^ 64-1 в 64-битной.

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

64

Другие решения

Как указано, смешивание unsigned а также signed может привести к неожиданному поведению (даже если оно четко определено).

Предположим, вы хотите перебрать все элементы вектора, кроме последних пяти, вы можете ошибочно написать:

for (int i = 0; i < v.size() - 5; ++i) { foo(v[i]); } // Incorrect
// for (int i = 0; i + 5 < v.size(); ++i) { foo(v[i]); } // Correct

предполагать v.size() < 5тогда как v.size() является unsigned, s.size() - 5 будет очень большое количество, и так i < v.size() - 5 было бы true для более ожидаемого диапазона значений i, И UB тогда происходит быстро (вне доступа один раз i >= v.size())

Если v.size() возвратил бы подписанное значение, тогда s.size() - 5 был бы отрицательным, и в вышеупомянутом случае условие было бы ложным немедленно.

С другой стороны, индекс должен быть между [0; v.size()[ так unsigned имеет смысл.
У Signed также есть своя собственная проблема, как UB с переполнением или определяемым реализацией поведением для сдвига вправо отрицательного числа со знаком, но менее частым источником ошибок для итерации.

33

Один из наиболее распространенных примеров ошибки — это когда вы MIX подписали и не подписали значения:

#include <iostream>
int main()  {
auto qualifier = -1 < 1u ? "makes" : "does not make";
std::cout << "The world " << qualifier << " sense" << std::endl;
}

Выход:

Мир не имеет смысла

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

Моделирование типа без знака на основе ожидаемой области значений ваших чисел является плохой идеей. Большинство чисел ближе к 0, чем к 2 миллиардам, поэтому с беззнаковыми типами многие ваши значения ближе к границе допустимого диапазона. Что еще хуже, окончательный значение может находиться в известном положительном диапазоне, но при оценке выражений промежуточные значения могут недооцениваться, и, если они используются в промежуточной форме, они могут быть ОЧЕНЬ неправильными значениями. Наконец, даже если ожидается, что ваши ценности всегда будут положительными, это не значит, что они не будут взаимодействовать с Другой переменные, которые Можно будьте отрицательны, и в результате вы столкнетесь с вынужденной ситуацией смешивания типов со знаком и без знака, что является худшим местом.

19

Почему использование неподписанного int чаще вызывает ошибки, чем использование подписанного int?

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

Используйте правильный инструмент для работы.

Что не так с модульной арифметикой? Разве это не ожидаемое поведение int без знака?
Почему использование неподписанного int чаще вызывает ошибки, чем использование подписанного int?

Если задача хорошо согласована: ничего страшного. Нет, не более вероятно.

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

Алгоритмы сжатия / распаковки также, как и различные графические форматы, выигрывают и менее подвержены ошибкам неподписанный математика

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


Целочисленная математика со знаком имеет интуитивно понятный вид и легко воспринимается всеми, включая учеников, занимающихся программированием. C / C ++ изначально не предназначался и не должен быть вводным языком. Для быстрого кодирования, в котором используются защитные сети от переполнения, лучше подходят другие языки. Для быстрого быстрого кода C предполагает, что кодеры знают, что они делают (они имеют опыт).

Ловушка подписанный математика сегодня является вездесущим 32-разрядным int что с таким количеством проблем достаточно широко для общих задач без проверки диапазона. Это приводит к самоуспокоенности, что переполнение не закодировано против. Вместо, for (int i=0; i < n; i++) int len = strlen(s); рассматривается как нормально, потому что n предполагается < INT_MAX и строки никогда не будут слишком длинными, вместо того, чтобы быть полностью защищенными в первом случае или используя size_t, unsigned или даже long long во 2-м.

C / C ++ развивался в эпоху, которая включала как 16-битные, так и 32-битные int и дополнительный бит без знака 16-битный size_t предоставляет было значительным. Внимание было необходимо в отношении проблем переполнения, будь то int или же unsigned,

С 32-битными (или более широкими) приложениями Google на не 16-битных int/unsigned платформы, позволяет не обращать внимания на переполнение +/- int учитывая его широкий диапазон. Это имеет смысл для таких приложений, чтобы поощрить int над unsigned, Еще int математика не очень хорошо защищена.

Узкий 16-битный int/unsigned проблемы применяются сегодня с некоторыми встроенными приложениями.

Рекомендации Google хорошо подходят для кода, который они пишут сегодня. Это не окончательное руководство для более широкого диапазона кода C / C ++.


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

В C / C ++ математическое переполнение со знаком имеет вид неопределенное поведение и поэтому не определенно легче обнаружить, чем определенное поведение неподписанный математика


Как @Chris Uzdavinis хорошо прокомментировал, смешивая подписанный а также неподписанный Лучше всего избегать его всеми (особенно начинающими) и, при необходимости, тщательно кодировать.

11

У меня есть некоторый опыт работы с руководством по стилю Google, AKA, Руководством автостопщика по безумным директивам от плохих программистов, которые давно в компании. Это конкретное руководство является лишь одним из десятков безумных правил в этой книге.

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

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

5

Использование беззнаковых типов для представления неотрицательных значений …

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

Руководство по кодированию Google делает упор на первый вид рассмотрения. Другие руководства, такие как Основные положения C ++, уделять больше внимания второму пункту. Например, рассмотрим основное руководство I.12:

I.12. Объявить указатель, который не должен быть нулевым, как not_null

причина

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

пример

int length(const char* p);            // it is not clear whether length(nullptr) is valid
length(nullptr);                      // OK?
int length(not_null<const char*> p);  // better: we can assume that p cannot be nullptr
int length(const char* p);            // we must assume that p can be nullptr

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

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

1
По вопросам рекламы [email protected]