Подтвердить что ты не робот

Как преобразовать произвольное значение double в целое число, избегая при этом поведения undefined?

Скажем, у меня есть функция, которая принимает 64-битное целое число, и я хочу позвонить он с double с произвольным числовым значением (т.е. он может быть очень большим в величина или даже бесконечность):

void DoSomething(int64_t x);

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

Параграф 1 из [conv.fpint] в стандарте С++ 11 гласит:

Значение типа с плавающей запятой может быть преобразовано в prvalue целочисленный тип. Преобразование транскрибируется; то есть дробная часть отбрасывается. Поведение undefined, если усеченное значение не может быть представлены в типе назначения.

Поэтому существует много значений d выше, которые вызовут undefined поведение. Я бы хотел, чтобы преобразование насыщалось, так что значения, превышающие 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 типа с плавающей точкой. Результат - точный если возможно. Если преобразованное значение находится в диапазоне значений который может быть представлен, но значение не может быть представлено точно, это выбор, определяемый реализацией либо следующей нижней, либо более высокое представимое значение.

Итак, clamped может все еще быть kint64max + 1, и поведение может по-прежнему быть undefined.

Каков самый простой переносимый способ сделать то, что я ищу? Бонусные баллы, если он также изящно обрабатывает NaN s.

Обновить. Чтобы быть более точным, я хотел бы, чтобы следующее было верно для int64_t SafeCast(double), которая решает эту проблему:

  • Для любого двойного d вызов SafeCast(d) не выполняет поведение undefined в соответствии со стандартом, и не выдает исключение или не отменяет.

  • Для любого двойного d в диапазоне [-2^63, 2^63) SafeCast(d) == static_cast<int64_t>(d). То есть, SafeCast согласуется с С++ правила преобразования, где бы ни было определено последнее.

  • Для любого двойного d >= 2^63, SafeCast(d) == kint64max.

  • Для любого двойного d < -2^63, SafeCast(d) == kint64min.

Я подозреваю, что истинная трудность здесь заключается в выяснении того, находится ли d в диапазон [-2^63, 2^63). Как обсуждалось в вопросе и в комментариях к другим ответов, я думаю, используя листинг от kint64max до double, чтобы проверить верхнюю bound является не стартером из-за поведения undefined. Это может быть более перспективным для используйте std::pow(2, 63), но я не знаю, гарантировано ли это в точности 2 ^ 63.

4b9b3361

Ответ 1

Оказывается, это проще сделать, чем я думал. Благодаря Майклу О'Рейли для основной идеи этого решения.

В основе вопроса лежит выяснение, будет ли усеченный двойной представляемый как 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;
}

Ответ 2

Здесь решение, которое не соответствует всем критериям, а также анализ для почему нет. Подробнее см. принятый ответ.

// 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;
}

Я считаю, что это избегает поведения undefined. Не стоит опасаться приведение целых чисел к удвоению в проверках диапазона. Предполагая разумность на пути что непредставляемые целые числа преобразуются (в частности, что отображение является монотонным), к моменту прохождения проверки диапазона мы можем быть уверены, что d находится в [-2^63, 2^63), как требуется для неявного броска в конце функция.

Я также уверен, что это правильно забивает значения диапазона.

Проблема - это критерии №2 от обновления к моему вопросу. Рассмотрим где kint64max не представляется двойным, но kint64max - 1 есть. Кроме того, предположим, что это реализация, где литье kint64max в двойное значение дает следующее нижнее представимое значение, т.е. kint64max - 1. Пусть d 2 ^ 63 - 2 (т.е. kint64max - 1). затем SafeCast(d) - kint64max, так как проверка диапазона преобразует kint64max в double, давая значение, равное d. Но static_cast<int64_t>(d) kint64max - 1.

Попробуйте, как я мог, я не могу найти способ решить эту проблему. Я также не могу написать unit test, который проверяет мои критерии без выполнения unit test undefined поведение. Я чувствую, что здесь есть более глубокий урок, о невозможности определить, приведет ли действие в системе undefined изнутри самой системы, не вызывая undefined поведение.

Ответ 3

Как насчет:

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 является следующим более высоким или меньшим представимым значением.