size_t или int для размеров, индекса и т. д.

15

В C ++ size_t(или, вернее, T::size_type«обычно» size_t; т. Е. unsignedТип) используется как возвращаемое значение для size()аргумента и operator[]т. Д. (См std::vector. И т. Д.)

С другой стороны, языки .NET используют int(и, необязательно long) для той же цели; фактически CLS-совместимые языки не обязаны поддерживать неподписанные типы .

Учитывая, что .NET новее, чем C ++, что-то подсказывает мне, что могут быть проблемы с использованием unsigned intдаже для вещей, которые «не могут быть» отрицательными, таких как индекс массива или длина. Является ли подход C ++ "историческим артефактом" для обратной совместимости? Или между этими двумя подходами существуют реальные и существенные компромиссные решения?

Почему это важно? Хорошо ... что я должен использовать для нового многомерного класса в C ++; size_tили int?

struct Foo final // e.g., image, matrix, etc.
{
    typedef int32_t /* or int64_t*/ dimension_type; // *OR* always "size_t" ?
    typedef size_t size_type; // c.f., std::vector<>

    dimension_type bar_; // maybe rows, or x
    dimension_type baz_; // e.g., columns, or y

    size_type size() const { ... } // STL-like interface
};
Ðаn
источник
6
Стоит отметить: в нескольких местах .NET Framework -1возвращается из функций, которые возвращают индекс, чтобы указать «не найден» или «вне диапазона». Это также возвращается из Compare()функций (реализации IComparable). 32-разрядный тип int считается типом go для общего числа, и я надеюсь, что это очевидные причины.
Роберт Харви

Ответы:

9

Учитывая, что .NET новее C ++, что-то подсказывает мне, что могут быть проблемы с использованием unsigned int даже для вещей, которые «не могут быть» отрицательными, например, индекс массива или длина.

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

sum = data[k - 2] + data[k - 1] + data[k] + data[k + 1] + ...

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

if (k - 2 < 0) {
    throw std::out_of_range("will never be thrown"); 
}

if (k < 2) {
    throw std::out_of_range("will be thrown"); 
}

if (k < 2uL) {
    throw std::out_of_range("will be thrown, without signedness ambiguity"); 
}

Вместо этого вы должны изменить свое выражение проверки диапазона. Это главное отличие. Программисты также должны помнить правила целочисленного преобразования. В случае сомнений перечитайте http://en.cppreference.com/w/cpp/language/operator_arithmetic#Conversions

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

C # действительно предназначен для тех приложений, которым не нужно более 2 ^ 31 элементов на массив. Например, приложению электронной таблицы не нужно иметь дело с таким количеством строк, столбцов или ячеек. C # имеет дело с верхним пределом, имея необязательную проверенную арифметику, которую можно включить для блока кода с ключевым словом, не связываясь с параметрами компилятора. По этой причине C # поддерживает использование целого числа со знаком. Когда эти решения рассматриваются в целом, это имеет смысл.

C ++ просто другой, и сложнее получить правильный код.

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

rwong
источник
Это точно моя ситуация; спасибо за конкретные примеры. (Да, я знаю, но это может быть полезно иметь «высшие власти» , чтобы цитировать.)
Ðаn
1
@Dan: если вам нужно что-то процитировать, этот пост будет лучше.
rwong
1
@Dan: Джон Регер активно исследует эту проблему на языках программирования. См. Blog.regehr.org/archives/1401
rwong
Существуют противоположные мнения: gustedt.wordpress.com/2013/07/15/…
rwong
14

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

size_t целочисленный размер с целью:

Тип size_tявляется целочисленным беззнаковым целочисленным типом реализации, который достаточно большой, чтобы содержать размер в байтах любого объекта. (Спецификация C ++ 11 18.2.6)

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

Обратите внимание, что вы всегда должны использовать, size_tесли ваш класс должен выглядеть и чувствовать себя как класс STL. Все классы STL в спецификации используют size_t. Это допустимо для компилятора typedef size_tбыть unsigned int, и это также допустимо для его определения типа unsigned long. Если вы используете intили longнапрямую, вы в конечном итоге столкнетесь с компиляторами, где человек, который думает, что ваш класс следует стилю STL, попадает в ловушку, потому что вы не следовали стандарту.

Что касается использования подписанных типов, есть несколько преимуществ:

  • Короткие имена - людям действительно легко печатать int, но гораздо сложнее загромождать код unsigned int.
  • Одно целое число для каждого размера. Существует только одно CLS-совместимое целое число из 32 битов, то есть Int32. В C ++ есть два ( int32_tи uint32_t). Это может упростить взаимодействие API

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

Для целевых приложений .NET не было такой сильной необходимости в полнодоменном индексе без знака. Многие из целей для таких чисел просто недопустимы в управляемом языке (на ум приходит пул памяти). Кроме того, как только вышел .NET, 64-битные компьютеры были явно в будущем. Мы далеки от того, чтобы нуждаться в полном диапазоне 64-битного целого числа, поэтому жертвовать одним битом не так болезненно, как это было раньше. Если вам действительно нужно 4 миллиарда индексов, вы просто переключаетесь на использование 64-битных целых чисел. В худшем случае вы запускаете его на 32-битной машине, и это немного медленно.

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

Корт Аммон - Восстановить Монику
источник
скажем, реализация size()была return bar_ * baz_;; разве это не создает потенциальную проблему с целочисленным переполнением (обходом), которого у меня не было бы, если бы я не использовал size_t?
Ðаn
5
@Dan Вы можете создать такие случаи, когда значение беззнаковых целочисленных значений будет иметь значение, и в этих случаях лучше всего использовать все языковые функции для его решения. Однако я должен сказать, что было бы интересно создать класс, в котором bar_ * baz_можно переполнить целое число со знаком, но не целое число без знака. Ограничивая себя C ++, стоит отметить, что переполнение без знака определено в спецификации, но переполнение со знаком является неопределенным поведением, поэтому, если желательна арифметика по модулю целых чисел без знака, определенно используйте их, потому что они действительно определены!
Корт Аммон - Восстановить Монику
1
@Dan - еслиsize() захлестнула подписанное умножение, вы на язык UB земли. (и в fwrapvрежиме, см. далее :) Когда затем , чуть-чуть больше, он переполнит беззнаковое умножение, вы окажетесь на земле пользовательского кода-ошибки - вы получите поддельный размер. Поэтому я не думаю, что неподписанные покупает здесь много.
Мартин Ба,
4

Я думаю, что ответ Руонга выше уже превосходно выдвигает на первый план проблемы.

Я добавлю свой 002:

  • size_tто есть размер, который ...

    может хранить максимальный размер теоретически возможного объекта любого типа (включая массив).

    ... требуется только для индексов диапазона sizeof(type)==1, если вы имеете дело с charтипами byte ( ). (Но, заметим, он может быть меньше, чем тип ptr :

  • Таким образом, xxx::size_typeможет использоваться в 99,9% случаев, даже если это тип со знаком размера. (сравнить ssize_t)
  • Тот факт , что std::vectorи друзья решили size_t, в неподписанных тип, для размера и индексации , по мнению некоторых , чтобы быть недостатком конструкции. Я согласен. (Серьезно, потратьте 5 минут и посмотрите молниеносный доклад CppCon 2016: Джон Калб «unsigned: Руководство по улучшению кода» .)
  • Когда вы разрабатываете C ++ API сегодня, вы находитесь в трудном положении: используйте, size_tчтобы соответствовать стандартной библиотеке, или используйте ( подписанный ) intptr_tили ssize_tдля простых и менее подверженных ошибкам расчетов индексации.
  • Не используйте int32 или int64 - используйте, intptr_tесли вы хотите подписать и хотите, чтобы размер машинного слова или использовался ssize_t.

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

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

  • данные правила целочисленного продвижения в C ++ ->
  • Типы без знака просто не подходят для «семантических» типов для чего-то вроде размера, который семантически не подписан.

Я повторю совет Джона здесь:

  • Выберите типы для операций, которые они поддерживают (не диапазон значений). (* 1)
  • Не используйте неподписанные типы в вашем API. Это скрывает ошибки без каких-либо преимуществ.
  • Не используйте «unsigned» для количеств. (* 2)

(* 1) то есть unsigned == bitmask, никогда не делайте математику с ней (здесь попадает первое исключение - вам может понадобиться счетчик, который переносит - это должен быть тип без знака.)

(* 2) количества, означающие что-то, на что вы рассчитываете и / или делаете математику

Мартин Ба
источник
Что вы имеете в виду под "полной доступной плоской памятью"? Кроме того, конечно, вы не хотите ssize_t, определяемый как подписанный кулон size_tвместо intptr_t, который может хранить любой (не член) указатель и, следовательно, может быть больше?
Дедупликатор
@Deduplicator - Ну, я думаю, что я, возможно, неправильно понял size_tопределение. Смотрите size_t против intptr и en.cppreference.com/w/cpp/types/size_t Узнали что-то новое сегодня. :-) Я думаю, что остальные аргументы верны, я посмотрю, смогу ли я исправить используемые типы.
Мартин Ба
0

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

используя подписанный int:

int32_t i = GetRandomNumberFromRange(-1000, 1000);

if (i < 0)
{
    //error
}

if (i > size())
{
    //error
}

используя unsigned int:

int32_t i = GetRandomNumberFromRange(-1000, 1000);

/// This will underflow any number below zero, so that it becomes a very big *positive* number instead.
uint32_t asUnsigned = static_cast<uint32_t>(i);

/// We now don't need to check for below zero, since an unsigned integer can only be positive.
if (asUnsigned > size())
{
    //error
}
Асгер
источник
1
Вы действительно хотите объяснить это более подробно.
Мартин Ба,
Чтобы сделать ответ более полезным, возможно, вы можете описать, как выглядят границы целочисленных массивов или сравнения смещений (со знаком и без знака) в машинном коде от различных поставщиков компиляторов. Есть много онлайн-компиляторов C ++ и сайтов разборки, которые могут показывать соответствующий скомпилированный машинный код для данного кода C ++ и флагов компилятора.
Rwong
Я попытался объяснить это еще немного.
asger