【问题标题】:Why is std::abs(9484282305798401ull) = 9484282305798400?为什么 std::abs(9484282305798401ull) = 9484282305798400?
【发布时间】:2017-05-31 14:25:13
【问题描述】:

我目前正在编写一个模板化的帮助方法,它可以将 C 数字(包括 unsigned long long)转换为 GMP 库中的 mpz_class 数字。在这期间,有一个电话到std::abs

然而,事实证明,对于 C++17 (g++ 6.3.1),

#include <iostream>
#include <cmath>

int main()
{
    std::cout << (unsigned long long)std::abs(9484282305798401ull);
}

给出9484282305798400 的错误输出。

据我从cmath 了解到,std::abs 首先将参数转换为双精度值。

根据 C++ 文档,double 有 52 个尾数位,这意味着在精度损失之前,我必须严格小于 2^52 = 4503599627370496 的最大整数值。

我是否正确地说,由于 9484282305798401 超过了这个限制,std::abs 最终会丢弃精度以给出不正确的答案?

为了澄清,我绝对知道要求无符号整数的绝对值是完全没有意义的。但是,我希望模板化函数适用于一般 C 数字,而不必分别为每个有符号和无符号类型专门创建一个特化。

【问题讨论】:

  • 如果可以,在无符号类型上调用 abs 有什么意义?
  • 我正在编写一个模板函数,该函数在调用 GMP 库的 mpz_import 类之前会经过一些算术运算;在这之间,可能需要对有符号数字的 std::abs 进行调用。
  • 请注意,虽然 IEEE 754 双精度确实使用 52 位来存储尾数,但实际上有 53 位,因为该值已标准化并且存在“隐式 1”(请参阅​​此 SO 帖子:@987654323 @)

标签: c++ c++11 floating-point precision c++17


【解决方案1】:

您的程序格式不正确。来自[c.math.abs]/29.9.2.3

如果使用X 类型的参数调用abs(),其中is_­unsigned_­v&lt;X&gt;true,并且如果X 不能通过整数提升转换为int,则程序是非良构的。

不过,编译器应该对此发出警告。

无论如何,在无符号类型上调用 std::abs 也没有任何意义。

【讨论】:

【解决方案2】:

首先,脱离上下文(获取无符号类型的绝对值),您所做的事情并没有真正意义。但我离题了。

您发布的代码无法编译。至少在我使用的编译器中没有(无论哪个 repl.it 使用)。相反,它抱怨一个模棱两可的过载。即使它确实编译了,它也会将unsigned long long 转换为不支持其实际值的不同类型(在本例中为double)。

像这样将abs 更改为llabs

std::cout << (unsigned long long)std::llabs(9484282305798401ull);

..都使它编译并产生准确的结果。请参阅有关整数类型 here 的不同 abs 函数的文档。

【讨论】:

  • libstdc++ used to accept this code 的旧版本我怀疑这些旧版本大部分都会消失。
  • 是的,但是 OP 声明他们使用的是 c++17,并且错误来自 c++11。但为了完整起见,我会将其包含在我的答案中。
  • 使用llabs的小问题是The behavior is undefined if the result cannot be represented by the return type
【解决方案3】:

如果您想以与标准库函数不同的方式管理无符号类型,您可以创建自己的 abs 重载:

#include <cmath>
#include <type_traits>

namespace my {

template <class S>
auto abs (S x) -> typename std::enable_if<std::is_signed<S>::value,
                                          decltype(std::abs(x))>::type
{
    return std::abs(x);
}

template <class U>
auto abs (U x) -> typename std::enable_if<std::is_unsigned<U>::value, U>::type
{
    return x;
}

} // namespace my

然后

std::cout << my::abs(9484282305798401ull) << '\n'              // -> 9484282305798401
          << my::abs(-3.14159) << '\n'                         // -> 3.14159
          << my::abs(std::numeric_limits<char>::min()) << '\n' // -> 128
          << my::abs(std::numeric_limits<int>::min()) << '\n'  // -> -2147483648

请注意,std::abs 提升 chars(在我的实现中签名),但由于 ints 的 2 补码表示,无法检索 INT_MIN 的绝对值。

【讨论】:

  • 如果你使用 SFINAE 会更好,这样不能提升为 unsigned long long 的类型也可以工作
猜你喜欢
  • 2014-02-18
  • 2013-04-12
  • 2011-02-02
  • 1970-01-01
  • 1970-01-01
  • 2021-08-06
  • 1970-01-01
  • 2011-03-08
  • 1970-01-01
相关资源
最近更新 更多