什么是最好的C ++方式来安全地模块化无符号整数?

时间:2014-07-17 05:39:54

标签: c++ portability undefined-behavior multiplication

假设您正在使用<cstdint>以及类似std::uint8_tstd::uint16_t的类型,并希望对其执行+=*=等操作。你喜欢对这些数字进行算术运算,就像在C / C ++中一样。这通常有效,您可以通过实验找到std::uint8_tstd::uint32_tstd::uint64_t,但不是std::uint16_t

具体而言,与std::uint16_t的乘法有时会失败,优化的构建会产生各种奇怪的结果。原因?由于有符号整数溢出导致的未定义行为。编译器基于未发生未定义行为的假设进行优化,因此开始从程序中修剪代码块。具体的未定义行为如下:

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

原因在于C ++的推广规则以及您和其他几乎所有人一样使用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 longunsigned long long,具体取决于平台。

但这真的很糟糕:它需要根据当前平台上int的大小知道哪种类型存在问题。有没有更好的方法可以在没有#if迷宫的情况下避免使用无符号整数乘法的未定义行为?

3 个答案:

答案 0 :(得分:9)

可能使用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;
}

Demo

修改:更简单:

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;
}

Demo

答案 1 :(得分:7)

这是一个相对简单的解决方案,对于无符号类型比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;
}

答案 2 :(得分:6)

关于在select a.id, a.log_at, max(b.log_at), count(1) from logs a join logs b on b.log_at >= a.log_at and b.log_at <= a.log_at+ '20 m'::interval group by 1, 2 having count(1) > 1 order by 1 为64位的系统上uint32_t * uint32_t乘法的情况的C解决方案的文章有一个我没想过的非常简单的解决方案:32 bit unsigned multiply on 64 bit causing undefined behavior?

转换为我的问题的解决方案很简单:

int

简单地将static_cast<std::uint16_t>(1U * x * x) 放在算术运算链的左侧会将第一个参数提升到更大的1Uunsigned int等级,然后依此类推。促销将确保答案既未签名且请求的位仍然存在。然后最终的演员表将其缩减回所需的类型。

这简单而优雅,我希望一年前我能想到它。感谢所有回复的人。