Вопрос по c++ – Почему size_t без знака?

55

Бьярн Страуструп написал на языке программирования C ++:

The unsigned integer types are ideal for uses that treat storage as a bit array. Using an unsigned instead of an int to gain one more bit to represent positive integers is almost never a good idea. Attempts to ensure that some values are positive by declaring variables unsigned will typically be defeated by the implicit conversion rules.

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

Еще одна важная статья Скотта МейерсаВот, Подводя итог, он рекомендует не использовать unsigned в интерфейсах, независимо от того, всегда ли значение положительное или нет. Другими словами,even if negative values make no sense, you shouldn't necessarily use unsigned.

@Nicol: Поскольку это неподписанное, которое используется в интерфейсах, против которых Мейерс рекомендует, и Страуструп, кажется, говорит, что это не очень хорошая идея в приведенной выше цитате. Jon
Почему это было бы "ошибкой"? сделать его без знака? Nicol Bolas
Ответ Альфа выглядит верным. Люди склонны использовать тот факт, что size_t является как стандартным, так и беззнаковым, и поэтому они должны использовать size_t или беззнаковые типы в своем собственном коде. Если ответ будет что-то вроде «size_t без знака по историческим причинам», то это немного уменьшит это обоснование. Jon
Соответствующая цитата Херба Саттераyoutu.be/Puio5dly9N8?t=2660 : & quot; Используйте int, если вам не нужно что-то другое, затем продолжайте использовать что-то подписанное, пока вам действительно не понадобится что-то другое, а затем прибегните к неподписанному. И да, к сожалению, это ошибка в STL и стандартной библиотеке, в которой мы используем индексы без знака. & Quot; Jon
Обратите внимание, что Stroustrup не создавал C. И в первые дни оптимизация пространства / производительности была очень важна, иначе большинство людей никогда не остановили бы кодирование в сборке. dbrank0

Ваш Ответ

4   ответа
1

Myth 1: std::size_t является неподписанным из-за устаревших ограничений, которые больше не применяются.

Есть два «исторических» Причины, обычно упоминаемые здесь:

sizeof returns std::size_t, which has been unsigned since the days of C. Processors had smaller word sizes, so it was important to squeeze that extra bit of range out.

Но ни одна из этих причин, несмотря на то, что она очень старая, на самом деле не относится к истории.

sizeof по-прежнему возвращаетstd::size_t который до сих пор не подписан. Если вы хотите взаимодействовать сsizeof или контейнеры стандартной библиотеки, которые вам придется использоватьstd::size_t.

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

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

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

Myth 2Вам не нужен этот дополнительный бит. (A.K.A., у вас никогда не будет строки размером более 2 ГБ, если ваше адресное пространство составляет всего 4 ГБ.)

Размеры и индексы предназначены не только для памяти. Ваше адресное пространство может быть ограничено, но вы можете обрабатывать файлы, которые намного больше, чем ваше адресное пространство. И хотя у вас может не быть строки с более чем 2 ГБ, вы можете с комфортом иметь битовый набор с более чем 2 Гбит. И не забудьте виртуальные контейнеры, предназначенные для разреженных данных.

Myth 3: Вы всегда можете использовать более широкий тип со знаком.

Не всегда. Это правда, что для локальной переменной или двумя, вы можете использоватьstd::int64_t (при условии, что ваша система имеет один) илиsigned long long и, вероятно, написать вполне разумный код. (Но вам все равно понадобятся некоторые явные приведения и проверка в два раза больше границ, иначе вам придется отключить некоторые предупреждения компилятора, которые могут предупредить вас об ошибках в другом месте вашего кода.)

Но что, если вы создаете большую таблицу индексов? Вы действительно хотите дополнительные два или четыреbytes для каждого индекса, когда вам нужен только одинbit? Даже если у вас достаточно памяти и современный процессор, увеличение этой таблицы вдвое может оказать вредное влияние на местность ссылок, а все проверки диапазона теперь выполняются в два этапа, снижая эффективность прогнозирования ветвлений. А что если тыdon't есть вся эта память?

Myth 4Арифметика без знака удивительна и неестественна.

Это подразумевает, чтоsigned арифметика не удивительна или как-то более естественна. И, возможно, именно в математическом мышлении все основные арифметические операции замкнуты над множеством целых чисел.

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

Это ошибка:

auto mid = (min + max) / 2;  // BUGGY

Еслиmin а такжеmax подписаны, сумма может переполниться, и это приводит к неопределенному поведению. Большинство из нас обычно пропускают такие ошибки, потому что мы забываем, что дополнение не закрыто для набора подписанных целых. Нам это сходит с рук, потому что наши компиляторы обычно генерируют код, который делает что-то разумное (но все же удивительное).

Еслиmin а такжеmax без знака, сумма может все еще переполниться, но неопределенное поведение исчезло. Вы все равно получите неправильный ответ, так что это все еще удивительно, но не более удивительно, чем это было с подписанными целыми.

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

Даже если бы вы могли исключить неподписанные типы из всех своих API, вам все равно нужно быть готовым к этим неподписанным & quot; сюрпризам & quot; если вы имеете дело со стандартными контейнерами или форматами файлов или проводными протоколами. Стоит ли добавлять трения в свои API для «решения»? только часть проблемы?

@Nolol Bolas: пример виртуальных контейнеров специально предназначен для противодействия конкретному аргументу, часто выдвигаемому никогда не подписанным лагерем: у вас никогда не будет контейнера с индексами, которые покрывают половину памяти.
@Nolol Bolas: & quot; Если я действительно знаю, что мои индексы никогда не будут больше некоторого размера, тогда я могу использовать соответствующий тип. & Quot; Правильный, а иногда и соответствующий тип не подписан.
& Quot;Do you really want an extra two or four bytes for every index when you need just one bit?& Quot; Откуда я знаю, что мне нужен только один бит? Если я действительно знаю, что мои показатели будутnever быть больше, чем какой-то размер, тогда я могу использовать соответствующий тип. Но если у меня есть таблица, в которой нужно хранить любой индекс, который может появиться в этой таблице, то она должна иметь возможность хранитьany index, Преждевременные оптимизации преждевременны.
& Quot;that you'll never have a container with indexes that cover half of memory.& Quot; Но это не аргумент. Аргумент в том, что у вас никогда не будет такого контейнера безknowing что ты пишешь такой контейнер. Никогда не будетvector или жеdeque или что угодно; это всегда будет специфическая структура данных, которая явно разработана для гигантских размеров. И поэтому вы будете использовать тип индекса, соответствующий ожидаемому размеру вашего контейнера.
& Quot;And don't forget virtual containers designed for sparse data.& Quot; И такие контейнеры будут использовать тип размера / индекса, который достаточно велик дл данных, которые они могут хранить. В 32-битной системе они все равно должны использовать 64-битные целые числа. Так же, как файловые API давно перестали использоватьint для размеров файлов. Даже API файловой системы C ++ 17 не полагается наsize_t для размеров файлов; он используетuintmax_t, Так что это все еще не является законной причинойsize_t быть неподписанным.
59

size_t не подписано по историческим причинам.

В архитектуре с 16-битными указателями, такой как «маленький» В модели DOS программирования было бы нецелесообразно ограничивать строки 32 КБ.

По этой причине стандарт C требует (через требуемые диапазоны)ptrdiff_tподписанный аналогsize_t и результирующий тип разности указателей должен составлять 17 бит.

Эти причины все еще могут применяться в некоторых частях мира встроенного программирования.

Однако они не применимы к современному 32-разрядному или 64-разрядному программированию, где гораздо более важным фактором является то, что неудачные правила неявного преобразования в C и C ++ превращают неподписанные типы в аттракторы ошибок, когда они используются для чисел (и следовательно, арифметические операции и сравнения величин). Оглядываясь назад, мы можем видеть, что решение принять те конкретные правила конвертации, например, гдеstring( "Hi" ).length() < -3 практически гарантировано, было довольно глупо и непрактично. Однако это решение означает, что в современном программировании принятие беззнаковых типов для чисел имеет серьезные недостатки и никаких преимуществ & # x2013; за исключением удовлетворения чувств тех, кто находитunsigned быть самоописательным именем типа и не думать оtypedef int MyType.

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

@ Алекс: Я понимаю твои чувства. Тем не менее, причина, по которой мы проводим строгую проверку типов в C ++, насколько это возможно при сохранении совместимости с C, заключается в том, что люди подвержены ошибкам. Существует даже очень известное название того, что происходит не так, когда вы просто делаете это возможным.
Я видел, что Java допустила ошибку, не включая неподписанный тип и такие вещи, как разбор0xffffffff или же0xffffffffffffffff тяжелее / медленнее или работа с беззнаковыми значениями в сети. Теперь они должны представить некоторые функции для поддержки операций без знака в Java 8.
Все хорошие компиляторы выдают предупреждение заstring( "Hi" ).length() < -3 но не для сравнения двух подписанных int; ваша жизнь не станет легче, еслиsize_t был определен как подписанный, вы просто будете делать различные виды ошибок.
Это очень большая проблема и для 32-битных систем. Вы не хотите ограничиваться размером 2 ГБ, если вы можете адресовать до 4 ГБ.
Я не согласен с "аттракторами ошибок" часть. C (++) - это не тот язык, на котором следует писать небрежно, делая предположения, прежде чем читать и понимать хорошую подробную книгу о языке или самом языковом стандарте. Я не думаю, что невежество является оправданием для обвинения в языковой особенности. Это там, с этим нужно иметь дело, хотят ли они этого или нет, если они его используют. Есть больше вещей о C (++) и других языках программирования, которые не работают. Возьмите с плавающей точкой, например. Многие начинают использовать его со всеми видами предположений, которые действительны только в обычной математике. FP - это ошибка?
3

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

В C вы можете иметь указатель, который указывает на массив. Действительный указатель может указывать на любой элемент массива или один элемент после конца массива. Он не может указывать на один элемент перед началом массива.

int a[2] = { 0, 1 };
int * p = a;  // OK
++p;  // OK, points to the second element
++p;  // Still OK, but you cannot dereference this one.
++p;  // Nope, now you've gone too far.
p = a;
--p;  // oops!  not allowed

C ++ соглашается и распространяет эту идею на итераторы.

Аргументы против неподписанных типов индексов часто приводят пример обхода массива задом наперед, и код часто выглядит так:

// WARNING:  Possibly dangerous code.
int a[size] = ...;
for (index_type i = size - 1; i >= 0; --i) { ... }

Этот код работаетonly еслиindex_type подписан, что используется в качестве аргумента, что типы индексов должны быть подписаны (и, что, по расширению, размеры должны быть подписаны).

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

// WARNING:  Bad code.
int a[size] = ...;
for (int * p = a + size - 1; p >= a; --p) { ... }

Yikes, теперь у нас есть неопределенное поведение! Игнорирование проблемы, когдаsize 0, у нас есть проблема в конце итерации, потому что мы генерируем недопустимый указатель, который указывает на элемент перед первым. Это неопределенное поведение, даже если мы никогда не пытаемся разыменовать этот указатель.

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

Правильное решение на основе указателей:

int a[size] = ...;
for (int * p = a + size; p != a; ) {
  --p;
  ...
}

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

Теперь по аналогии решение на основе индекса становится:

int a[size] = ...;
for (index_type i = size; i != 0; ) {
  --i;
  ...
}

Это работает лиindex_type подписан или не подписан, но выбор без знака дает код, который более точно сопоставляется с версиями идиоматического указателя и итератора. Без знака также означает, что, как и в случае с указателями и итераторами, мы сможем получить доступ к каждому элементу последовательности - мы не уступаем половину нашего возможного диапазона, чтобы представлять бессмысленные значения. Хотя это не является практической проблемой в 64-битном мире, это может быть очень реальной проблемой в 16-битном встроенном процессоре или в создании абстрактного типа контейнера для разреженных данных в огромном диапазоне, который все еще может обеспечить идентичный API как родной контейнер.

26

size_t являетсяunsigned потому что отрицательные размеры не имеют смысла.

(Из комментариев :)

Это не столько обеспечение, сколько указание того, что есть. Когда вы в последний раз видели список размером -1? Следуйте этой логике слишком далеко, и вы обнаружите, что unsigned вообще не должно существовать, и битовые операции также не должны быть разрешены. & # X2013;geekosaur

Более того: адреса, по причинам, о которых вы должны подумать, не подписаны. Размеры генерируются путем сравнения адресов; обработка адреса как подписанного сделает очень неправильную вещь, а использование значения со знаком для результата приведет к потере данных таким образом, что, очевидно, ваше чтение цитаты Страуструпа приемлемо, но на самом деле это не так. Возможно, вы можете объяснить, что вместо этого должен делать отрицательный адрес. & # X2013;geekosaur

@Jon: предупреждение дает вам знать, что существует вероятность ошибки во время выполнения, и ее следует исправить. Опять же, если вы исправите это (либо заставив функцию принять значение со знаком int, либо убедившись, что отрицательные значения не могут быть переданы), проблем не будет. И если вы не исправите это, если вы просто выполните приведение, чтобы закрыть компилятор, то вы заслуживаете того, что получили.
Пункт Страуструпа (и Мейера) заключается в том, что если значение не может быть отрицательным, это не означает, что вы должны сделать его беззнаковым. Во-первых, вы больше не можете обнаружить ошибочные отрицательные значения, передаваемые в интерфейсах (которые неявно преобразуются). Jon
Разве это не то, к чему обращался Страуструп при написании "Попытки обеспечить положительные значения некоторых значений, объявив переменные без знака ..."? Jon
@NicolBolas: Мой компилятор не выдает здесь предупреждения:size_t x = 0; for(size_t i=10; i>=x; --i) {} -- Does yours?
Если это не ваш ответ (size_t существует для сравнения адресов), а не «отрицательные размеры не имеют смысла»? Последнее, кажется, противоречит тому, что заявили Страуструп и Мейерс. Jon

Похожие вопросы