#c #unsigned #underflow
Вопрос:
Я понимаю, что существует правило, по которому числа с шириной меньше, чем int
можно перевести в более широкий тип для операции сложения. Но я не могу полностью объяснить, как только одна из следующих перестановок print_unsafe_minus
потерпит неудачу. Как получается, что только <unsigned, long>
пример терпит неудачу, и каково преимущество для программистов в отношении лучших практик?
#include <fmt/core.h>
template<typename M, typename N>
void print_unsafe_minus() {
M a = 3, b = 4;
N c = a - b;
fmt::print("{}n", c);
}
int main() {
// storing result of unsigned 3 minus 4 to a signed type
print_unsafe_minus<uint8_t, int8_t>(); // -1
print_unsafe_minus<uint16_t, int8_t>(); // -1
print_unsafe_minus<uint32_t, int8_t>(); // -1
print_unsafe_minus<uint64_t, int8_t>(); // -1
print_unsafe_minus<uint8_t, int16_t>(); // -1
print_unsafe_minus<uint16_t, int16_t>(); // -1
print_unsafe_minus<uint32_t, int16_t>(); // -1
print_unsafe_minus<uint64_t, int16_t>(); // -1
print_unsafe_minus<uint8_t, int32_t>(); // -1
print_unsafe_minus<uint16_t, int32_t>(); // -1
print_unsafe_minus<uint32_t, int32_t>(); // -1
print_unsafe_minus<uint64_t, int32_t>(); // -1
print_unsafe_minus<uint8_t, int64_t>(); // -1
print_unsafe_minus<uint16_t, int64_t>(); // -1
print_unsafe_minus<uint32_t, int64_t>(); // 4294967295
print_unsafe_minus<uint64_t, int64_t>(); // -1
}
(правка) Также стоит отметить-если мы расширим пример, включив в него 128-битные целые числа, то следующие две перестановки также завершатся неудачей:
print_unsafe_minus<uint32_t, __int128>(); // 4294967295
print_unsafe_minus<uint64_t, __int128>(); // 18446744073709551615
Комментарии:
1. Вы знаете, что именно говорится в правилах акции? Все эти примеры следует объяснить, точно следуя им. Когда вы пытаетесь интерпретировать правила в этих случаях, можете ли вы объяснить свою интерпретацию и чем именно она отличается от того, что происходит?
2. @NateEldredge, если
uint8_t
иuint16_t
получат повышение за добавление к той же ширине ,uint32_t
что и, то не должны ли они также потерпеть неудачу, как это происходит дляint64_t
?3. что нужно сделать программистам в отношении лучших практик? не смешивайте подписанное и неподписанное. На самом деле
unsigned
типы нужны только для побитовых операций.4.
only the <unsigned, long>
из вашего кода толькоuint32_t, int64_t
сбой, а неunsigned, long
5. @NathanOliver Ну, есть типы контейнеров STL с их раздражающими
size_t
вещами!
Ответ №1:
Прежде чем мы начнем, давайте предположим, что OP использует реализацию с 32-разрядным int
типом. То int32_t
есть эквивалентно int
.
Пусть X-ширина M, а Y — ширина N.
Давайте разделим ваши тестовые случаи на три категории:
Первая категория: X
Здесь применяется целочисленное продвижение, которое всегда выполняется перед вызовом арифметического оператора.
uint8_t
и uint16_t
все их диапазоны значений представимы int
, поэтому они повышаются int
до того, как выполнять вычитание. Затем вы получаете знаковое значение -1
from doing 3 - 4
, которое затем используется для инициализации целочисленного типа со знаком, который может храниться независимо от его ширины -1
. Таким образом, вы получаете -1
в качестве вывода.
Вторая категория: (X >= 32) и (X >>= Y)
Никакого продвижения по службе не происходит до выполнения вычитания.
Здесь применяется правило, согласно которому целочисленная арифметика без знака всегда выполняется по модулю 2 X, где X-ширина целого числа.
Следовательно a - b
, всегда давайте вам 2 X — 1, так как это значение равно -1 по модулю 2 в диапазоне M
.
Теперь вы назначаете его подписанному типу. Предположим, что C 20 (до C 20 это поведение, определяемое реализацией, при присвоении значения без знака, которое не может быть представлено целевым типом со знаком).
Здесь результат a - b
(т. е. 2 X — 1) преобразуется в уникальное значение, соответствующее самому себе по модулю 2 Y в диапазоне назначения (т. е. от -2 Y-1 до 2 Y-1 — 1). Поскольку X >= Y, это всегда будет > -1
.
Таким образом, вы получаете -1
в качестве вывода.
Третья категория: (X >= 32) и (X >
В этой категории есть только один случай, а именно случай , когда M= uint32_t
, N = uint64_t
.
Вычитание такое же, как и в категории 2, где вы получаете 2 32 — 1.
Правило преобразования в подписанный тип остается прежним. Однако на этот раз 2 32 — 1 равно самому себе по модулю 2 64, поэтому значение остается неизменным.
Примечание: 4294967295 == 2 32 — 1
унести
Это, вероятно, удивительный аспект C , и, как предложил @NathanOliver, вам следует избегать смешивания типов со знаком и без знака и проявлять крайнюю осторожность, когда вы хотите их смешать.
Вы можете указать компилятору генерировать предупреждения для такого преобразования, включив -Wconversion
. Ваш код получает много предупреждений, когда это включено.
Комментарии:
1. Итак, допустим, я хочу оставить это
c = a - b;
в своем коде, зная, что в настоящее время это безопасно, но что в какой-то момент в будущем другой разработчик может изменить размеры подписанного c или неподписанного a или b. Какой static_assert будет инкапсулировать здесь принципы, по которым он остается «безопасным», т. Е. Не перетекает из отрицательного в положительное?2. @PatrickParker Ну, я думаю, что это заслуживает отдельного вопроса и дополнительных исследований. Так что поправьте меня, если я ошибаюсь, вам в основном нужно следующее: учитывая два целочисленных типа, вы хотите получить во время компиляции знаковый тип, который может содержать любое значение разницы между типами операндов.
3. Ну, это больше похоже на следующее: разработчик A управляет заголовком, который определяет тип операндов со знаком и без знака, которые могут измениться. разработчик B контролирует код
signed_c = unsigned_a - unsigned_b
, который не изменится. и третий разработчик (я) хотел бы написать static_assert, используя эти переменные и/или символы ввода, чтобы убедиться, что результат не будет переполнен, чтобы стать положительным числом. Вы правы в том, что это, вероятно, должен быть отдельный вопрос. Но это было то, к чему я пытался подойти со своим вопросом «что такое еда на вынос».4. можете ли вы просто подтвердить, правильно ли этот static_assert предотвращает этот переход к положительному сценарию?
static_assert(sizeof(a-b)>=sizeof(c) || sizeof(a-b)<sizeof(int));
правильно ли было писатьint
здесь вместо этого32
?5. @PatrickParker Вы берете разницу между двумя значениями без знака и преобразуете ее в значение со знаком, результат может быть не тем, что вы хотите (я сделал оговорку об этом ранее в более раннем комментарии). Например,
UINT_MAX- 0u
и0u - 1u
оба дали бы вамUINT_MAX
. Вам придется привестиa
иb
к знаковому типу шире, чем оба, прежде чем выполнять вычитание (т. Е.uint32_t
кint64_t
)
Ответ №2:
Давайте предположим, что вменяемая двухкомпонентная платформа, int
имеющая 32 бита и uint32_t
такая же, как unsigned
.
uint32_t a = 3, b = 4;
int64_t c = a - b;
Операнды -
оператору подвергаются целочисленным повышениям*. int
не могут представлять все значения uint32_t
, но 32-разрядные unsigned
могут представлять все значения uint32_t
. Эти ценности пропагандируются unsigned
. Тип результата — -
это общий тип операндов после продвижения — оба операнда являются unsigned
. Тип результата -
оператора является unsigned
. a - b
это математически -1
. Результат таков (unsigned)-1
, но unsigned
не может представлять отрицательные числа. Таким -1
образом, преобразуется в unsigned
тип, он «оборачивается» и приводит к UINT_MAX, который равен UINT32_MAX, потому unsigned
что имеет 32 бита. Этот результат представим в int64_t
, поэтому преобразование не происходит, и c
ему присваивается значение UINT32_MAX.
Для контраста возьмем, к примеру <uint16_t, int64_t>
. 32-разрядный int
может представлять все значения an uint16_t
, поэтому он uint16_t
повышается до int, поэтому результатом a - b
является только an (int)-1
. Преобразование из (int)-1
в беззнаковое число не выполняется. Затем int64_t
может представлять -1
, поэтому значение -1
просто присваивается переменной с типом int64_t
.
* На языке Си это называется целочисленными акциями…
Комментарии:
1. и что нужно сделать программистам в отношении лучших практик? например, «не беспокойтесь о назначении неподписанного a-b подписанному c, если c не шире int и не отличается шириной (a-b)»?
2. Ну, я не понимаю этой части. Лучшие практики для чего? Каким должен быть результат? Если вы хотите иметь
-1
, просто напишите-1
вместо вычитания двух чисел. Определенно, лучшая практика — выучить язык и знать, как он работает. Если вам не нравятся неявные рекламные акции, конверсии и слабая типизация, перейдите на другой язык программирования.3.
don't worry about assigning unsigned a-b to signed c unless c is wider than int and different width than (a-b)
Какой результат вы хотите получить? Код работает так, как задумано. Я полагаю, что вы хотите быть явным —N c = (M)(a - b);
— в этом случае он будет переполнен в соответствии с типом без знака (я не знаю, является ли это предполагаемым результатом ). Все зависит от того, чего вы хотите . В любом случае лучшей практикой может быть соблюдение некоторых правил MISRA и других стандартов безопасности, которые специально регулируют запутанное поведение неявных рекламных акций в коде.4. Я вижу пример правила 5-0-3 в MISRA C 2008 tlemp.com/download/rule/MISRA-CPP-2008-STANDARD.pdf . Исходя из этого, другой наилучшей практикой было бы выполнить
N c = (N)a - (N)b
приведение значений перед вычислением. Но обратите внимание, что переполнение со знаком также является угловым случаем — лучше всего было бы обрабатывать его отдельно.