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

jac*_*bsa 13 c++ type-conversion undefined-behavior language-lawyer

假设我有一个接受64位整数的函数,我想用一个double带有任意数值的函数调用它(即它的幅度非常大,甚至无限大):

void DoSomething(int64_t x);

double d = [...];
DoSomething(d);
Run Code Online (Sandbox Code Playgroud)

C++ 11标准中[conv.fpint]的第1段说明了这一点:

可以将浮点类型的prvalue转换为整数类型的prvalue.转换转发; 也就是说,丢弃小数部分.如果截断的值无法在目标类型中表示,则行为未定义.

因此,d上面有许多值会导致未定义的行为.我希望转换为饱和,因此大于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);
Run Code Online (Sandbox Code Playgroud)

但是,标准的下一段说明了这一点:

可以将整数类型或无范围枚举类型的prvalue转换为浮点类型的prvalue.如果可能,结果是准确的.如果要转换的值在可以表示的值范围内,但该值无法准确表示,则它是实现定义的下一个较低或较高可表示值的选择.

所以clamped可能仍然存在kint64max + 1,行为可能仍未定义.

什么是最简单的便携式方式来做我正在寻找的东西?奖励积分,如果它也优雅地处理NaNs.

更新:更确切地说,我希望以下内容对于int64_t SafeCast(double)解决此问题的 函数都是如此:

  1. 对于任何double d,调用SafeCast(d)不会根据标准执行未定义的行为,也不会抛出异常或以其他方式中止.

  2. 对于d范围内的任何双倍[-2^63, 2^63), SafeCast(d) == static_cast<int64_t>(d).也就是说,SafeCast在定义后者的任何地方都同意C++的转换规则.

  3. 任何双d >= 2^63,SafeCast(d) == kint64max.

  4. 任何双d < -2^63,SafeCast(d) == kint64min.

我怀疑这里的真正困难在于弄清楚是否d在范围内[-2^63, 2^63).正如在问题和对其他答案的评论中所讨论的那样,我认为使用kint64maxto double来测试上限是由于未定义的行为而导致的.它可能更有希望使用std::pow(2, 63),但我不知道这是否保证正好是2 ^ 63.

jac*_*bsa 5

事实证明,这比我想象的要简单。感谢 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;
}
Run Code Online (Sandbox Code Playgroud)