c++ 如何在避免未定义行为的同时将任意双精度型转换为整数?

cuxqih21  于 2023-02-06  发布在  其他
关注(0)|答案(5)|浏览(166)

假设我有一个接受64位整数的函数,我想用一个double来调用它,这个double可以是任意的数值(也就是说,它的大小可能非常大,甚至是无穷大):

void DoSomething(int64_t x);

double d = [...];
DoSomething(d);

C++11标准中[conv.fpint]的第1段是这样说的:
浮点型的纯右值可以转换为整型的纯右值。转换截断;也就是说,小数部分被丢弃。2如果被截断的值不能在目标类型中表示,则该行为是未定义的。
因此,d的很多值会导致未定义的行为,我希望转换为saturate,这样大于std::numeric_limits<int64_t>::max()(下面称为kint64max)的值,包括无穷大,就变成了那个值,类似地,最小可表示值也是如此,这似乎是自然的方法:

double clamped = std::min(d, static_cast<double>(kint64max));
clamped = std::max(clamped, static_cast<double>(kint64min));
DoSomething(clamped);

但是,标准中的下一段是这样说的:
整数类型或无作用域枚举类型的纯右值可以转换为浮点类型的纯右值。如果可能,结果将是精确的。如果要转换的值在可以表示的值范围内,但无法精确表示该值,则将由实现定义选择下一个较低或较高的可表示值。
所以clamped可能仍然是kint64max + 1,行为可能仍然没有定义。
什么是最简单的便携方式来做我正在寻找的?加分,如果它也优雅地处理NaN的。

更新:更准确地说,我希望解决此问题的int64_t SafeCast(double)函数满足以下条件:

1.对于任何双精度d,调用SafeCast(d)不会执行标准中未定义的行为,也不会引发异常或中止。
1.对于[-2^63, 2^63)SafeCast(d) == static_cast<int64_t>(d)范围内的任何双精度值d。也就是说,SafeCast符合C++的转换规则,无论后者定义在何处。
1.对于任何双d >= 2^63SafeCast(d) == kint64max
1.对于任何双d < -2^63SafeCast(d) == kint64min
我怀疑这里真正的困难是弄清楚d是否在[-2^63, 2^63)的范围内。正如在问题和其他答案的注解中所讨论的,我认为使用kint64maxdouble的强制转换来测试上限是不可行的,因为行为未定义。使用std::pow(2, 63)可能更有希望。但我不知道这是否能保证正好是2^63

a64a0gku

a64a0gku1#

事实证明,这比我想象的要简单得多。感谢Michael O'Reilly提出了这个解决方案的基本思想。
问题的核心是判断截断的双精度型是否可以表示为int64_t,使用std::frexp可以很容易地做到这一点:

#include <cmath>
#include <limits>

static constexpr int64_t kint64min = std::numeric_limits<int64_t>::min();
static constexpr int64_t kint64max = std::numeric_limits<int64_t>::max();

int64_t SafeCast(double d) {
  // We must special-case NaN, for which the logic below doesn't work.
  if (std::isnan(d)) {
    return 0;
  }

  // Find that exponent exp such that
  //     d == x * 2^exp
  // for some x with abs(x) in [0.5, 1.0). Note that this implies that the
  // magnitude of d is strictly less than 2^exp.
  //
  // If d is infinite, the call to std::frexp is legal but the contents of exp
  // are unspecified.
  int exp;
  std::frexp(d, &exp);

  // If the magnitude of d is strictly less than 2^63, the truncated version
  // of d is guaranteed to be representable. The only representable integer
  // for which this is not the case is kint64min, but it is covered by the
  // logic below.
  if (std::isfinite(d) && exp <= 63) {
    return d;
  }

  // Handle infinities and finite numbers with magnitude >= 2^63.
  return std::signbit(d) ? kint64min : kint64max;
}
a11xaf1n

a11xaf1n2#

这里有一个不符合所有标准的解决方案,沿着原因分析。请参阅the accepted answer以获得更好的答案。

// Define constants from the question.
static constexpr int64_t kint64min = std::numeric_limits<int64_t>::min();
static constexpr int64_t kint64max = std::numeric_limits<int64_t>::max();

int64_t SafeCast(double d) {
  // Handle NaN specially.
  if (std::isnan(d)) return 0;

  // Handle out of range below.
  if (d <= kint64min) return kint64min;

  // Handle out of range above.
  if (d >= kint64max) return kint64max;

  // At this point we know that d is in range.
  return d;
}

我相信这可以避免未定义的行为。在范围检查中将整数转换为双精度型没有什么需要警惕的。假设转换不可表示整数的方式是合理的(特别是Map是单调的),那么在范围检查结束时,我们可以确定d[-2^63, 2^63)中,这是函数末尾隐式转换所需要的。
我也确信这会正确地钳制超出范围的值。
问题是我的问题的更新中的标准#2。考虑kint64max不能表示为双精度型,但kint64max - 1可以的实现。此外,假设这是一个将kint64max转换为双精度型会产生下一个较低的可表示值的实现。例如kint64max - 1,令d为2^63 - 2(即kint64max - 1),则SafeCast(d)kint64max,因为范围检查将kint64max转换为双精度,得到的值等于d,但static_cast<int64_t>(d)kint64max - 1
尽管我可能尝试过,但我找不到解决这个问题的方法。我甚至不能编写一个检查我的标准的单元测试,而单元测试不执行未定义的行为。我觉得这里有一个更深层次的教训要学习--关于不可能检测系统中的一个操作是否会在系统本身内部导致未定义的行为,而不导致未定义的行为。

7vux5j2d

7vux5j2d3#

这里有一个不使用std::frexp的解决方案,它利用uint64_t来处理棘手的情况。

#include <algorithm>
#include <cmath>
#include <cstdint>
#include <limits>

int64_t SafeCast(double d) {
    if (std::isnan(d)) {
        return 0;
    }

    if (d < 0) {
        // Easy case to clamp, because std::numeric_limits<int64_t>::min() is
        // exactly representable as IEEE double.
        return static_cast<int64_t>(std::max<double>(d, std::numeric_limits<int64_t>::min()));
    }

    // Convert to uint64_t, clamping to range [0..2^63].
    uint64_t u = static_cast<uint64_t>(std::min<double>(d, static_cast<uint64_t>(1) << 63));

    // Clamp to int64_t.
    return std::min(u, (static_cast<uint64_t>(1) << 63) - 1U);
}
rn0zuynd

rn0zuynd4#

不如这样:

constexpr uint64_t weird_high_limit = (double)kint64max == (double)(kint64max-1);
int64_t clamped = (d >= weird_high_limit + kint64max)? kint64max: (d <= kint64min)? kint64min: int64_t(d);

我认为这考虑到了所有的边缘情况,如果d < (double)kint64max,那么(exact)d <= (exact)kint64max,证明通过(double)kint64max是下一个更高或更低的可表示值这一事实的矛盾来进行。

相关问题