Подтвердить что ты не робот

Какой лучший способ С++ для многократного умножения целых чисел без знака?

Скажем, что вы используете <cstdint> и типа типа std::uint8_t и std::uint16_t, и хотите сделать на них операции типа += и *=. Вы хотите, чтобы арифметика на этих числах обертывалась модульно, как обычно в C/С++. Обычно это работает, и вы экспериментально работаете с std::uint8_t, std::uint32_t и std::uint64_t, но не std::uint16_t.

В частности, умножение с std::uint16_t иногда неэффективно, с оптимизированными сборками, производящими все виды странных результатов. Причина? Undefined из-за целочисленного переполнения со знаком. Компилятор оптимизируется на основе предположения о том, что поведение Undefined не происходит, и поэтому начинает обрезать куски кода из вашей программы. Конкретное поведение Undefined заключается в следующем:

std::uint16_t x = UINT16_C(0xFFFF);
x *= x;

Причина в правилах продвижения на С++ и в том, что вы, как и почти все остальные в наши дни, используете платформу, на которой std::numeric_limits<int>::digits == 31. То есть int - это 32-разрядный (digits счетчик бит, но не знаковый бит). x получает повышение до signed int, несмотря на то, что оно без знака, и 0xFFFF * 0xFFFF переполнения для 32-разрядной арифметики со знаком.

Демонстрация общей проблемы:

// Compile on a recent version of clang and run it:
// clang++ -std=c++11 -O3 -Wall -fsanitize=undefined stdint16.cpp -o stdint16

#include <cinttypes>
#include <cstdint>
#include <cstdio>

int main()
{
     std::uint8_t a =  UINT8_MAX; a *= a; // OK
    std::uint16_t b = UINT16_MAX; b *= b; // undefined!
    std::uint32_t c = UINT32_MAX; c *= c; // OK
    std::uint64_t d = UINT64_MAX; d *= d; // OK

    std::printf("%02" PRIX8 " %04" PRIX16 " %08" PRIX32 " %016" PRIX64 "\n",
        a, b, c, d);

    return 0;
}

Вы получите хорошую ошибку:

main.cpp:11:55: runtime error: signed integer overflow: 65535 * 65535
    cannot be represented in type 'int'

Как избежать этого, конечно, нужно бросить как минимум unsigned int перед умножением. Только точный случай, когда число бит неподписанного типа точно равно половине числа бит int, проблематично. Любое меньшее приведет к тому, что умножение не сможет переполняться, как при std::uint8_t; любое большее приведет к тому, что тип будет точно сопоставлен с одним из ранжирования продвижения, как с std::uint64_t совпадением unsigned long или unsigned long long в зависимости от платформы.

Но это действительно отстой: для этого требуется знать, какой тип проблематичен, исходя из размера int на текущей платформе. Есть ли лучший способ, с помощью которого можно избежать Undefined поведения с беззнаковым целочисленным умножением без #if mazes?

4b9b3361

Ответ 1

Эта статья о решении C для случая uint32_t * uint32_t умножения на систему, в которой int равна 64 бит, имеет действительно простое решение, о котором я не думал: 32-битное беззнаковое умножается на 64-битное поведение undefined?

Это решение, переведенное на мою проблему, прост:

static_cast<std::uint16_t>(1U * x * x)

Простое включение 1U в левой части цепочки арифметических операций, подобных этому, будет продвигать первый параметр в более высокий ранг unsigned int и std::uint16_t, а затем вниз по цепочке. Продвижение гарантирует, что ответ будет беззнаковым и что запрошенные биты останутся. Окончательное литье затем уменьшает его до нужного типа.

Это действительно просто и элегантно, и мне жаль, что я не подумал об этом год назад. Спасибо всем, кто раньше ответил.

Ответ 2

Некоторые шаблоны метапрограммирования с помощью SFINAE, возможно.

#include <type_traits>

template <typename T, typename std::enable_if<std::is_unsigned<T>::value && (sizeof(T) <= sizeof(unsigned int)) , int>::type = 0>
T safe_multiply(T a, T b) {
    return (unsigned int)a * (unsigned int)b;
}

template <typename T, typename std::enable_if<std::is_unsigned<T>::value && (sizeof(T) > sizeof(unsigned int)) , int>::type = 0>
T safe_multiply(T a, T b) {
    return a * b;
}

Демо.

Изменить: проще:

template <typename T, typename std::enable_if<std::is_unsigned<T>::value, int>::type = 0>
T safe_multiply(T a, T b) {
    typedef typename std::make_unsigned<decltype(+a)>::type typ;
    return (typ)a * (typ)b;
}

Демо.

Ответ 3

Здесь относительно простое решение, которое заставляет рекламу unsigned int вместо int для неподписанного типа более узкого, чем int. Я не думаю, что какой-либо код генерируется promote, или, по крайней мере, не более кода, чем стандартная целая реклама; он просто заставит умножение и т.д. использовать неподписанные операционные системы вместо подписанных:

#include <type_traits>
// Promote to unsigned if standard arithmetic promotion loses unsignedness
template<typename integer> 
using promoted =
  typename std::conditional<std::numeric_limits<decltype(integer() + 0)>::is_signed,
                            unsigned,
                            integer>::type;

// function for template deduction
template<typename integer>
constexpr promoted<integer> promote(integer x) { return x; }

// Quick test
#include <cstdint>
#include <iostream>
#include <limits>
int main() {
  uint8_t i8 = std::numeric_limits<uint8_t>::max(); 
  uint16_t i16 = std::numeric_limits<uint16_t>::max(); 
  uint32_t i32 = std::numeric_limits<uint32_t>::max(); 
  uint64_t i64 = std::numeric_limits<uint64_t>::max();
  i8 *= promote(i8);
  i16 *= promote(i16);
  i32 *= promote(i32);
  i64 *= promote(i64);

  std::cout << " 8: " << static_cast<int>(i8) << std::endl
            << "16: " << i16 << std::endl
            << "32: " << i32 << std::endl
            << "64: " << i64 << std::endl;
  return 0;
}