Почему std::ssize принудительно устанавливается на минимальный размер для его типа размера со знаком?

В C++20 для получения подписанный размер контейнера для универсального кода. (И причина его добавления объясняется здесь.)

Несколько странно, что приведенное здесь определение (в сочетании с common_type и ptrdiff_t) заставляет возвращаемое значение быть либо ptrdiff_t, либо подписанной формой возвращаемого значения size() контейнера, в зависимости от того, что больше.

P1227R1 косвенно предлагает обоснование это (для std::ssize() было бы катастрофой преобразовать размер 60 000 в размер -5 536).

Однако мне кажется, что это странный способ попытаться это исправить.

  • Containers which intentionally define a uint16_t size and are known to never exceed 32,767 elements will still be forced to use a larger type than required.
    • The same thing would occur for containers using a uint8_t size and 127 elements, respectively.
    • В среде рабочего стола вам, вероятно, все равно; но это может быть важно для встроенных или других сред с ограниченными ресурсами, особенно если результирующий тип используется для чего-то более постоянного, чем переменная стека.
  • Контейнеры, которые используют размер по умолчанию size_t на 32-разрядных платформах, но которые, тем не менее, содержат элементы размером от 2 до 4 миллиардов, столкнутся с той же проблемой, что и выше.
  • Если все еще существуют платформы, для которых ptrdiff_t меньше 32 бит, они также столкнутся с той же проблемой.

Не лучше ли было бы просто использовать подписанный тип как есть (без увеличения его размера) и assert, чтобы не произошла ошибка преобразования (например, чтобы результат не был отрицательным)?

Я что-то упускаю?


Чтобы немного расширить это последнее предложение (вдохновленное ответом Никола Боласа): если бы оно было реализовано так, как я предложил , то этот код будет Just Work™:

void DoSomething(int16_t i, T const& item);

for (int16_t i = 0, len = std::ssize(rng); i < len; ++i)
{
    DoSomething(i, rng[i]);
}

Однако в текущей реализации это приводит к предупреждениям и/или ошибкам, если только static_casts не добавляются явно, чтобы сузить результат ssize, или использовать вместо этого int i, а затем сузить его в вызове функции (и индексации диапазона), ни одно из которых не кажется как улучшение.


person Miral    schedule 22.05.2019    source источник
comment
Как насчет тех контейнеров, которые имеют до 255 или 65535 элементов?   -  person n. 1.8e9-where's-my-share m.    schedule 22.05.2019
comment
Сегодня 32-битный микроконтроллер в большинстве случаев дешевле 8-битного. Во многих из них дешевле использовать 32 бита (размер регистра), чем 8 бит, и большая часть доступа к памяти по-прежнему выровнена по 32 битам, поэтому я не думаю, что можно много выиграть, используя int16_t вместо int32_t в этих случаях.   -  person Mirko    schedule 22.05.2019
comment
Для переменных стека нет. Если в конечном итоге он будет сохранен в какой-то структуре данных (что не невозможно, если поверх него будет наложено достаточное количество шаблонного кода), то это может произойти.   -  person Miral    schedule 22.05.2019
comment
Это библиотечная функция, каждая реализация может делать то, что считает нужным. На практике это почти никогда не имеет значения, но любой, кто поддерживает шаткую модель памяти, должен серьезно подумать о создании исключения.   -  person Hans Passant    schedule 22.05.2019
comment
Меня больше удивляет обратное, что контейнерам по-прежнему разрешено возвращать size() что-либо, кроме (любого псевдонима) std::size_t   -  person Caleth    schedule 22.05.2019


Ответы (1)


Контейнеры, которые преднамеренно определяют размер uint16_t и, как известно, никогда не превышают 32 767 элементов, по-прежнему будут вынуждены использовать больший тип, чем требуется.

Это не похоже на то, что контейнер хранит размер как этот тип. Преобразование происходит через доступ к значению.

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

Кроме того, не существует стандартного способа узнать, какой размер диапазона «заведомо никогда не превышается». decltype(size(range)) это то, о чем вы можете попросить; диапазоны размеров не требуются для обеспечения функции max_size. Без такой возможности самое безопасное предположение состоит в том, что диапазон, тип размера которого равен uint16_t, может принимать любой размер в пределах этого диапазона. Таким образом, размер со знаком должен быть достаточно большим, чтобы сохранить весь этот диапазон в виде значения со знаком.

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

Контейнеры, которые используют размер по умолчанию size_t на 32-разрядных платформах, но которые, тем не менее, содержат элементы размером от 2 до 4 миллиардов, столкнутся с той же проблемой, что и выше.

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

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

Это не улучшение.

И нет, просто утверждение/проверка контракта не является жизнеспособным решением. Смысл ssize состоит в том, чтобы заставить for(int i = 0; i < std::ssize(rng); ++i) работать без жалоб компилятора на несоответствие знаковых/беззнаковых. Чтобы получить утверждение из-за сбоя преобразования, которого не должно было произойти (кстати, его нельзя исправить без использования std::size, чего мы пытаемся избежать), который в конечном итоге не имеет отношения к вашему алгоритму ? Это ужасная идея.


если бы это было реализовано так, как я предложил, то этот код был бы Just Work™:

Оставим в стороне вопрос о том, как часто пользователь будет писать этот код.

Причина, по которой ваш компилятор будет ожидать/требовать от вас использования приведения, заключается в том, что вы запрашиваете опасную по своей сути операцию: вы потенциально теряете данные. Ваш код "Just Works™" работает только в том случае, если текущий размер соответствует int16_t; что делает преобразование статически опасным. Это не то, что должно происходить неявно, поэтому компилятор предлагает/требует, чтобы вы явно запросили это. И у пользователей, смотрящих на этот код, появляется большое бельмо на глазу, напоминающее им, что делается опасная вещь.

Это все к лучшему.

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

Опасные операции должны быть обозначены как таковые. Так как ssize маленький и его трудно заметить по замыслу, он должен быть максимально безопасным. В идеале он должен быть таким же безопасным, как size, но в противном случае он должен быть небезопасным только в той степени, в какой его невозможно сделать безопасным.

Пользователи не должны смотреть на использование ssize как на что-то сомнительное или сбивающее с толку; они не должны бояться использовать его.

person Nicol Bolas    schedule 22.05.2019
comment
Что касается первого пункта, я имею в виду кортежи и варианты (созданные автоматически как часть универсального кода, который становится все более распространенным), сохраняющие значение. В этом случае у разработчика может не быть возможности явно указать тип. И да, я действительно пытался сказать, что половина любого диапазона размеров небезопасна, если ее можно рассматривать как подписанную. Почему диапазоны меньше, чем ptrdiff_t, получают здесь особое внимание, а все, что равно или больше, — нет? - person Miral; 23.05.2019
comment
Что касается второго пункта, идея заключалась в том, что если алгоритм использует ssize, вероятно, его следует рассматривать как логическую ошибку при использовании этого алгоритма с недопустимо большой структурой данных. Если разработчик позволяет данным становиться достаточно большими, чтобы обернуть размер со знаком, то он должен использовать структуру данных с большим типом возврата size() (и, следовательно, также с большим типом возврата ssize()). Опять же, я не понимаю, почему это специально для только подмножества размеров. - person Miral; 23.05.2019
comment
Наконец, что, если разработчик хочет написать for (int16_t i = 0, len = std::ssize(rng); i < len; ++i), чтобы он мог передать i чему-то явно 16-битному внутри тела цикла без сужения static_cast? В текущей реализации это либо вызовет предупреждение, либо не скомпилируется. С моим предложением это просто работает. - person Miral; 23.05.2019
comment
@Miral: Почему диапазоны меньше, чем ptrdiff_t, получают здесь особое внимание, а все, что равно или больше, — нет? Потому что ломать код неправильно. Я не понимаю, почему это используется в специальном регистре только для подмножества размеров. Это в специальном регистре, потому что эти особые случаи не могут поддерживаться. Мы поддерживаем то, что можем, а то, что не можем, не поддерживаем. - person Nicol Bolas; 23.05.2019
comment
@Miral: чтобы они могли передать i чему-то явно 16-битному в теле цикла без сужающего static_cast? Сужающее приведение очень хорошо, так что каждый, кто смотрит на код, может сказать: "Эй, что это такое?" произойдет, если это переполнится?. Сам факт использования ssize не должен вызывать этот вопрос. Кроме того, что заставляет вас думать, что люди чаще пишут версию int16_t, а не версию int? - person Nicol Bolas; 23.05.2019
comment
Преобразование неподписанных 32-разрядных в подписанные 64-разрядные вполне возможно, поэтому может поддерживаться. Но он не поддерживается, поскольку может быть больше, чем int, что может быть неудобно. Фундаментальное предположение make_signed заключается в том, что вы используете только значения из общего диапазона обоих типов. Притворяться иначе глупо. Независимо от размера, переполнение не должно происходить, если только разработчик не использует неподходящую структуру данных для своих данных, если мы сейчас хотим заявить, что для общих алгоритмов безопасно использовать ssize для большинства структур данных. - person Miral; 23.05.2019
comment
@Miral: Ваша проблема в том, что вы хотите, чтобы ssize было make_signed версией size. Это не то, что это, и это не то, для чего это. И да, идея состоит в том, что использование ssize должно быть безопасным, за исключением случаев, когда законно невозможно сделать эту операцию безопасной. То есть ssize максимально безопасен. - person Nicol Bolas; 23.05.2019
comment
Если это не то, для чего он предназначен, то он кажется плохо названным. - person Miral; 23.05.2019
comment
@Мирал: Почему? Большинству пользователей на законных основаниях не важен размер шрифта. Они заботятся о том, чтобы отключить предупреждение компилятора о сравнениях со знаком/без знака. Их не следует наказывать за то, что они хотят, чтобы это было удалено, поскольку их общий код потенциально может быть нарушен только потому, что какой-то пользователь решил использовать size_type меньшего размера для своей функции size. - person Nicol Bolas; 23.05.2019