Почему Qt использует целочисленный тип со знаком для своих классов контейнеров?

Вопрос очевиден.

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

Я думал, что они хотят разрешить это для какой-то сумасшедшей формы индексирования, но она кажется неподдерживаемой?

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

По какой-то причине это просто кажется несовместимым с STL по дизайну...

Ответ 1

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

Единственное подходящее время для использования беззнаковых чисел - это арифметика модуля. Используя "unsgined" как своего рода спецификатор контракта, "число в диапазоне [0..." просто неуклюжие и слишком грубые, чтобы быть полезным.

Рассмотрим: какой тип следует использовать для представления идеи о том, что число должно быть положительным целым числом от 1 до 10? Почему 0... 2 ^ x более специальный диапазон?

Ответ 2

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

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

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

Иногда поведение переполнения является даже желательным, поскольку поведение переполнения арифметики без знака делает определенные проверки диапазона выразимыми как одно сравнение, которое в противном случае потребовало бы двух сравнений. Если я хочу проверить, находится ли x в диапазоне [a,b] и все значения без знака, я могу просто сделать:

if (x - a < b - a) {
}

Это не работает со знаковыми переменными; такие проверки диапазона довольно распространены для размеров и смещений массива.

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

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

Ответ 3

Этот ответ больше о поддержке основного вопроса: почему Qt, С# и другие решили использовать подписанный тип для вещей, которые по определению не имеют отрицательных значений?

В отношении подписанных/неподписанных типов на ум приходят две вещи:

  1. Без знака не имеет отрицательных значений по определению

    Так? Таким образом, если существует значение домена, где любое отрицательное значение не имеет смысла, тогда следует использовать тип без знака. Размер контейнера является таким значением. Нет смысла передавать -1 в качестве аргумента для resize() или operator[] (да, я знаю, есть причудливое поведение Python и, возможно, другие языки).

    С точки зрения пользователя:

    • Что произойдет, если я -1 в качестве аргумента [] или resize()
    • Возможно ли, что size() < 0? Что если есть?

    С точки зрения разработчика:

    • Что должен делать код, если аргумент [] или resize() равен <0? Бросить исключение, ничего не делать или разрешить взорвать приложение?

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

  2. Int со знаком ограничивает число значений только 1<<31 == 2147483648

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

  3. Отрицательная индексация

    (отключено одной ошибкой)

    Если вы хотите иметь индексирование относительно конца [-1] тогда можно реализовать очень понятный и защищенный от ошибок интерфейс. Подписанные значения для этого не требуются:

// simple strictly typed wrapper for any value. Implement and name it as you would like  to. 
using FromEnd = StrictTypeWrapper<std::size_t, struct FromEndTag>;

//the use it like this
auto value = container[FromEnd{1}];

Так в чем же смысл подписанного типа для вещей, которые не являются отрицательными по определению?