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

时间:2014-09-15 22:25:57

标签: c++ type-conversion language-lawyer undefined-behavior

我们说我有一个接受64位整数的函数,我想打电话 它带有double具有任意数值(即它可能非常大) 幅度,甚至是无限的):

void DoSomething(int64_t x);

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

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

  

浮点类型的prvalue可以转换为a的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);

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

  

整数类型或无范围枚举类型的prvalue可以是   转换为浮点类型的prvalue。结果是准确的   如果可能的话。如果转换的值在值的范围内   可以表示,但价值不能准确表示,   它是一个实现定义的选择下一个较低或   更高的代表值。

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

什么是最简单的便携式方式来做我正在寻找的东西?奖金积分如果 它还优雅地处理NaN s。

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

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

  2. 对于d范围内的任何双[-2^63, 2^63)SafeCast(d) == static_cast<int64_t>(d)。也就是说,SafeCast同意C ++的观点 转换规则无论在何处定义。

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

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

  5. 我怀疑这里的真正困难在于弄清d是否在 范围[-2^63, 2^63)。正如问题和对其他人的评论中所讨论的那样 答案,我认为使用kint64maxdouble的演员来测试鞋面 由于未定义的行为,bound是非启动的。它可能更有希望 使用std::pow(2, 63),但我不知道这是否保证完全正确 2 ^ 63

4 个答案:

答案 0 :(得分:2)

事实证明这比我想象的要简单。感谢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;
}

答案 1 :(得分:1)

这是一个不符合所有标准的解决方案,以及分析 为什么不。有关更好的答案,请参阅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;
}

我相信这可以避免未定义的行为。没有什么可以警惕的 在范围检查中将整数转换为双精度数。假设道路上的理智 转换不可表示的整数(特别是映射 是单调的),到范围检查过去时,我们可以确定d[-2^63, 2^63)中,根据隐含的强制转换的要求 功能

我也相信这会正确地限制超出范围值。

问题是从我的问题更新的标准#2。考虑一下 kint64max无法表示为double的实现,但是 kint64max - 1是。此外,假设这是一个实现 将kint64max转换为double会产生下一个较低的可表示值, 即kint64max - 1。设d为2 ^ 63 - 2(即kint64max - 1)。然后 SafeCast(d)kint64max,因为范围检查会将kint64max转换为 一个double,产生一个等于d的值。但是static_cast<int64_t>(d)kint64max - 1

尽我所能,我找不到解决这个问题的方法。我甚至不能写一个 检查我的标准的单元测试,没有单元测试执行undefined 行为。我觉得这里有更深刻的教训 - 有些东西 关于检测系统中的动作是否会导致的不可能性 来自系统内部的未定义行为,不会导致未定义 行为。

答案 2 :(得分:-1)

怎么样:

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是下一个更高或更低的可表示值的事实相矛盾。

答案 3 :(得分:-1)