【问题标题】:What's the best C++ way to multiply unsigned integers modularly safely?安全地将无符号整数相乘的最佳 C++ 方法是什么?
【发布时间】:2014-09-07 20:37:08
【问题描述】:

假设您正在使用<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 迷宫,是否有更好的方法可以避免无符号整数乘法的未定义行为?

【问题讨论】:

  • @TC:这听起来效率很低。 64 位乘法可能很慢。
  • 后续问题:是什么规则/促销使x *= x;uint16_t 提升为int32_t?我在 Std 中找到了一些提升规则,但无法准确地将它们映射到此。
  • @BenVoigt 标准中有一条错综复杂的路径,即由于有符号整数溢出,两个无符号短路的乘法导致无符号短路会为某些数据值产生未定义的行为。标准中有更直接的语句说,对相同的无符号数据类型产生相同类型结果的算术运算不可能溢出,结果就像模数运算并被使用,这是任何合理的程序员所期望的,包括我自己。标准自相矛盾。
  • @amdn: uint8_t(a) * uint8_t(b) 不对无符号类型进行算术运算,因此控制无符号算术的子句不适用。出乎意料,但确实如此。
  • 当您需要复杂的解决方案(例如该问题的答案)以将句法糖添加到对标准的直接语义解释中时,丹麦的某些东西已经腐烂了。

标签: c++ portability undefined-behavior multiplication


【解决方案1】:

这篇关于uint32_t * uint32_t 乘法在int 是64 位的系统上的C 解决方案的文章有一个我没有想到的非常简单的解决方案:32 bit unsigned multiply on 64 bit causing undefined behavior?

翻译成我的问题的解决方案很简单:

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

仅将1U 包含在算术运算链的左侧,这样会将第一个参数提升到unsigned intstd::uint16_t 的更大等级,然后在链下以此类推。提升将确保答案是无符号的并且请求的位仍然存在。最后的转换然后将其还原为所需的类型。

这真的很简单和优雅,我希望我在一年前就想到了。感谢之前回复的所有人。

【讨论】:

  • 此外,此解决方案适用于 C 和 C++,避免了所有模板的疯狂
  • @Nayuki 是的;添加C版本回答
【解决方案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;
}

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.

【讨论】:

  • &lt;decltype(a)&gt;::max() &gt; &lt;decltype(a * b)&gt;::max() / &lt;decltype(b)&gt;::max()(使用适当的演员表)时,我认为可能有效的方法是通过std::numeric_limits检测,因为maxconstexpr函数。如果是这样,请将每个参数转换为 typename std::make_unsigned&lt;decltype(+a))&gt;::type 而不仅仅是 unsigned int。这应该抓住每一个案例。 (make_unsigneddecltype 中的一元 + 用于确定提升类型。)无条件转换为提升类型的 make_unsigned 也可以,并且在正常平台上应该同样快。
  • @Myria 关于提升类型的无条件 make_unsigned 的优点。仍然需要保留enable_if,这样如果您传递签名类型,这将不起作用。
【解决方案3】:

这是一个相对简单的解决方案,对于比int 窄的无符号类型,强制升级为unsigned 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;
}

【讨论】:

  • 酷,我喜欢这个。在我看来,不是第 6 行的“未签名”,而是应该说 typename std::make_unsigned&lt;integer&gt;::type。另外,为了强制提升,你也可以只使用一元+;换句话说,+integer()。当我写这个问题时,我很好地理解了这个问题,我只是不明白这些很酷的模板技巧,我感谢你和@T.C.为了。 =)
  • @Myria std::make_unsigned&lt;integer&gt;::type 在这里不起作用。如果你真的想使用它,它需要是std::make_unsigned&lt;decltype(+a)&gt;::type。此外,这将为签名类型编译,这可能不是一个好主意,所以你需要一个 enable_if 某处:template&lt;typename integer&gt; using promoted = typename std::enable_if&lt;std::is_unsigned&lt;integer&gt;::value, typename std::conditional&lt;/*...*/&gt;::type&gt;::type;
  • @t.c.只需通过添加 &&!is_signed: 来更改布尔表达式
  • @rici 如果你这样做,它仍然可以编译(尽管不再强制转换)。我认为当你不打算强制无符号算术时调用类似promote的东西应该被认为是一个错误,所以最好在编译时诊断它。
猜你喜欢
  • 1970-01-01
  • 2013-04-07
  • 2012-03-23
  • 1970-01-01
  • 1970-01-01
  • 2012-06-17
  • 2015-04-05
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多