Реализация сложения по модулю 2^32 и проблемы переполнения?

Есть нужда в реализации сложения по модулю 2^32.

Первым делом появилась идея - просто сложить два числа (uint32_t); после произойдет переполнение, но результатом останется 4 младших байта, что и должно быть результатом сложения по модулю 2^32.

Но не станет ли это случайно "undefined behavior"? Если да, то как можно реализовать такое сложение иным способом?


Ответы (2 шт):

Автор решения: Stanislav Volodarskiy

static_cast<uint32_t>(static_cast<uint64_t>(a) + static_cast<uint64_t>(b)) выполнит сложение двух операндов типа uint32_t по модулю 232 без неопределённого поведения. У компилятора достаточно информации, чтобы на всех популярных процессорах выполнить сложение в одну ассемблерную инструкцию - приведения не приведут к вставке дополнительных инструкций в код.

→ Ссылка
Автор решения: VladD

Согласно документации, вычисления, проводимые над всеми беззнаковыми типами, выполняются по модулю 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(...)>).

→ Ссылка