Реализация сложения по модулю 2^32 и проблемы переполнения?
Есть нужда в реализации сложения по модулю 2^32.
Первым делом появилась идея - просто сложить два числа (uint32_t); после произойдет переполнение, но результатом останется 4 младших байта, что и должно быть результатом сложения по модулю 2^32.
Но не станет ли это случайно "undefined behavior"? Если да, то как можно реализовать такое сложение иным способом?
Ответы (2 шт):
static_cast<uint32_t>(static_cast<uint64_t>(a) + static_cast<uint64_t>(b))
выполнит сложение двух операндов типа uint32_t
по модулю 232 без неопределённого поведения. У компилятора достаточно информации, чтобы на всех популярных процессорах выполнить сложение в одну ассемблерную инструкцию - приведения не приведут к вставке дополнительных инструкций в код.
Согласно документации, вычисления, проводимые над всеми беззнаковыми типами, выполняются по модулю 2ⁿ, где n — количество бит, и не приводя к неопределённому поведению (кроме деления на ноль, конечно) .
Таким образом, вам достаточно работать с типом uint32_t
, как вы и предполагали.
А вот работа со знаковыми типами (например, int32_t
) опаснее, так как переполнение в этом случае является неопределённым поведением.
Не забудьте даже литералы (наподобие 1) привести к типу uint32_t
, иначе компилятор может вывести у результирующего выражения не тот тип, который вам нужен (например, знаковый).
Впрочем, некоторые компиляторы имеют ключи компиляции, которые заставляют знаковые типы подчиняться тем же правилам, что и беззнаковые. Например, у gcc есть опция -fwrapv
. Но при этом ваш код перестанет быть переносимым.
Дополнение: обсуждение в комментариях убедило меня, что работа с uint32_t
таки может в экзотических случаях приводить к неопределённому поведению. Для проверки неэкзотичности вашего случая вставьте такой вот assert:
#include <cstdint>
#include <type_traits>
static_assert(std::is_unsigned_v<decltype(+(uint32_t)0)>);
Для всех «обычных» систем этот assert проходит.
Что делать, если ваша система окажется экзотической?
- Для MSVC, система будет неэкзотической.
- Для GCC/Clang, используйте ключ
-fwrapv
, он предотвратит UB. - Если размер
int
на вашей системе больше 32 бит, считайте всё вunsigned int
, в конце (один раз) отрежьте ненужные биты. - Если размер
long
на вашей системе 32 бит, считайте в unsigned long (но при этом ваша система должна быть неэкзотической) - Если размер
int
меньше 32 бит, а размерlong
больше, считайте вunsigned long
, в конце (один раз) отрежьте ненужные биты.
Смотрите, что за экзотические случаи, и откуда они могут взяться. Дело в том, что C++ перед сложением приводит числовые типы размером меньше int
к типу int
или unsigned int
, а символьные типы (например, char32_t
) к типу int
, unsigned int
, long int
, unsigned long int
, long long int
или unsigned long long int
.
В нормальных системах uint32_t
является алиасом на unsigned int
или unsigned long int
, и проблем не возникает. Но стандарт не запрещает делать глупости, и определить, например, uint32_t
алиасом на char32_t
, который имеет право быть приведён к int
или там long int
.
(Точные правила этой всей фантасмагории описаны в стандарте, глава 7.3.7.)
Да, а ещё на экзотической системе может не оказаться типа uint32_t
.
Предлагаемый assert как раз и проверяет, что значения типа uint32_t
((uint32_t)0
) после integral promotion (+(uint32_t)0
) будут всё ещё иметь по крайней мере беззнаковый тип (std::is_unsigned_v<decltype(...)>
).