Можно ли нести разницу между двумя объектами size_t?

Я изучаю стандарт для своей команды, используя size_t vs int (или long и т.д.). Самый большой недостаток, который я видел, заключается в том, что разница между двумя объектами size_t может вызвать проблемы (я не уверен в конкретных проблемах - возможно, что-то не было дополнено 2s, а подписанный /unsigned сердитый компилятор). Я написал быструю программу на С++, используя компилятор V120 VS2013, который позволил мне сделать следующее:

#include <iostream>

main()
{
    size_t a = 10;
    size_t b = 100;
    int result = a - b;
}

В результате программы -90, которая, хотя и правильная, вызывает у меня беспокойство по поводу несоответствий типов, проблем с подписью/без знака или просто поведения undefined, если size_t используется для использования в сложной математике.

Мой вопрос: безопасно ли выполнять математику с объектами size_t, в частности, принимая разницу? Я рассматриваю использование size_t в качестве стандарта для таких вещей, как индексы. Я видел несколько интересных сообщений по этой теме, но они не затрагивают математическую проблему (или я ее пропустил).

Какой тип для вычитания 2 size_t?

typedef для подписанного типа, который может содержать size_t?

Ответы

Ответ 1

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

Вычитая два std::size_t, вы получите новый std::size_t и его значение будет определяться путем обертывания. В вашем примере, если 64 бит size_t, a - b будет равен 18446744073709551526. Это не соответствует (обычно используемому 32-битовому) int, поэтому назначенное значение реализации присваивается result.

Честно говоря, я бы рекомендовал не использовать целые числа без знака для чего-либо, кроме бит-магии. Несколько членов стандартного комитета согласны со мной: https://channel9.msdn.com/Events/GoingNative/2013/Interactive-Panel-Ask-Us-Anything 9:50, 42:40, 1:02:50

Правило большого пальца (перефразируя Чандлера Каррута из вышеприведенного видео): Если вы можете считать это самостоятельно, используйте int, в противном случае используйте std::int64_t.


Если его ранг преобразования меньше, чем int, например. если std::size_t - unsigned short. В этом случае результатом будет int, и все будет работать нормально (если int не шире, чем short). Однако

  • Я не знаю ни одной платформы, которая делает это.
  • Это все равно будет специфичным для платформы, см. первый абзац.

Ответ 2

Если вы не используете size_t, вы ввернуты: size_t - это тот тип, который существует для использования для размеров памяти, и, следовательно, гарантированно всегда будет достаточно большим для этой цели. (uintptr_t довольно схож, но он не является первым таким типом и не используется стандартными библиотеками и не доступен без включения stdint.h.) Если вы используете int, вы можете получить undefined когда ваши распределения превышают 2GiB адресного пространства (или 32kiB, если вы находитесь на платформе, где int - всего 16 бит!), даже если машина имеет больше памяти, и вы выполняете ее в режиме 64 бит.

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

Ответ 3

Тип size_t не имеет знака. Вычитание любых двух значений size_t определено-поведение

Однако, во-первых, результат определяется реализацией, если большее значение вычитается из меньшего. Результатом является математическое значение, приведенное к наименьшему положительному вычету по модулю SIZE_T_MAX + 1. Например, если наибольшее значение size_t равно 65535, а результат вычитания двух значений size_t равен -3, тогда результат будет 65536 - 3 = 65533. На другом компиляторе или машине с другим size_t, числовое значение будет отличаться.

Во-вторых, значение size_t может быть вне диапазона типа int. Если это так, мы получаем второй результат, определяемый реализацией, возникающий в результате принудительного преобразования. В этой ситуации может применяться любое поведение; это просто должно быть документировано реализацией, и преобразование не должно прерываться. Например, результат может быть зажат в диапазоне int, создавая INT_MAX. Общее поведение, наблюдаемое на двух машинах с дополнениями (практически все) при преобразовании беззнаковых типов с более широкой (или равной шириной) в более узкие типы подписей, - это простое усечение битов: из числа без знака берется достаточно бит для заполнения знакового значения, включая его знак бит.

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

Например, предположим, что вычитание пары значений 64 бит size_t на машине с двумя дополнениями дает абстрактное арифметическое значение -3, которое становится положительным значением 0xFFFFFFFFFFFFFFFD. Когда это принудительно внедряется в 32-разрядный int, тогда общее поведение, наблюдаемое во многих компиляторах для двух машин дополнения, заключается в том, что нижние 32 бита берутся за изображение полученного int: 0xFFFFFFFD. И, конечно же, это всего лишь значение -3 в 32 бит.

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

За исключением того, что расширение знака не возникает, когда значение расширяется при преобразовании из без знака в подпись. Таким образом, одной из проблем является редкая ситуация, при которой int шире, чем size_t. Если результат 16 бит size_t равен 65533, из-за вычитания 4 из 1 это не приведет к возникновению -3 при преобразовании в 32 бит int; он произведет 65533!