Ближайшее целое число к значению с плавающей запятой в C++03

Как я могу найти для некоторого целочисленного типа значение, наиболее близкое к некоторому значению типа с плавающей запятой, даже если значение с плавающей запятой находится далеко за пределами представляемого диапазона целого числа.

Или точнее:

Пусть F будет типом с плавающей запятой (возможно, float, double или long double). Пусть I будет целочисленным типом.

Предположим, что и F, и I имеют допустимую специализацию std::numeric_limits<>.

Учитывая представимое значение F и используя только C++03, как я могу найти ближайшее представимое значение I?

Мне нужно чистое, эффективное и потокобезопасное решение, которое ничего не предполагает о платформе, кроме того, что гарантируется C++03.

Если такого решения не существует, можно ли его найти, используя новые возможности C99/C++11?

Использование lround() из C99 кажется проблематичным из-за нетривиального способа сообщения об ошибках домена. Можно ли перехватывать эти ошибки домена переносимым и потокобезопасным способом?

Примечание. Я знаю, что Boost, вероятно, предлагает решение через свой шаблон boost::numerics::converter<>, но из-за его высокой сложности и многословности я не смог извлечь из него самое необходимое, и поэтому я не смог проверить, является ли их решение делает предположения за пределами С++ 03.

Следующий наивный подход терпит неудачу из-за того, что результат I(f) не определен C++03, когда неотъемлемая часть f не является представимым значением I.

template<class I, class F> I closest_int(F f)
{
  return I(f);
}

Тогда рассмотрим следующий подход:

template<class I, class F> I closest_int(F f)
{
  if (f < std::numeric_limits<I>::min()) return std::numeric_limits<I>::min();
  if (std::numeric_limits<I>::max() < f) return std::numeric_limits<I>::max();
  return I(f);
}

Это также терпит неудачу, потому что неотъемлемые части F(std::numeric_limits<I>::min()) и F(std::numeric_limits<I>::max()) могут все еще не быть представлены в I.

Наконец, рассмотрим этот третий подход, который также терпит неудачу:

template<class I, class F> I closest_int(F f)
{
  if (f <= std::numeric_limits<I>::min()) return std::numeric_limits<I>::min();
  if (std::numeric_limits<I>::max() <= f) return std::numeric_limits<I>::max();
  return I(f);
}

На этот раз I(f) всегда будет иметь четко определенный результат, однако, поскольку F(std::numeric_limits<I>::max()) может быть намного меньше, чем std::numeric_limits<I>::max(), вполне возможно, что мы вернем std::numeric_limits<I>::max() для значения с плавающей запятой, которое является несколькими целыми значениями меньше std::numeric_limits<I>::max().

Обратите внимание, что все проблемы возникают из-за того, что не определено, округляется ли преобразование F(i) вверх или вниз до ближайшего представимого значения с плавающей запятой.

Вот соответствующий раздел из С++ 03 (4.9 Преобразования с плавающей запятой):

Rvalue целочисленного типа или типа перечисления может быть преобразовано в rvalue типа с плавающей запятой. Результат точен, если это возможно. В противном случае это определяемый реализацией выбор следующего более низкого или более высокого представляемого значения.


person Kristian Spangsege    schedule 26.09.2012    source источник
comment
Красиво написанный вопрос. Я хочу, чтобы они все выглядели так.   -  person Robert Harvey    schedule 27.09.2012
comment
@AlexeyFrunze Я хочу «float -> int», однако в моих слабых попытках сделать это я преобразовываю максимальные и минимальные целые числа в числа с плавающей запятой, и цитата предназначалась для освещения этого последнего преобразования в обратном направлении. Я постараюсь внести изменения, которые сделают это более понятным.   -  person Kristian Spangsege    schedule 27.09.2012
comment
@AlexeyFrunze Ты снова удалил свой вопрос? Или я что-то напутал?   -  person Kristian Spangsege    schedule 27.09.2012
comment
Извините, я удалил комментарий, как только понял проблему.   -  person Alexey Frunze    schedule 27.09.2012
comment
Я знаю, что это не главное в вашей проблеме, но разве вы не должны использовать I (f + 0,5), если вам нужно ближайшее целое число вместо усеченной целой части?   -  person Vaughn Cato    schedule 27.09.2012
comment
@VaughnCato Думаю, +.5 можно легко вставить: f += (f >= 0) ? +0.5 : -0.5;.   -  person Alexey Frunze    schedule 27.09.2012


Ответы (1)


У меня есть практическое решение для radix-2 (двоичных) типов с плавающей запятой и целых типов до 64-битных и более длинных. Смотри ниже. Комментарии должны быть четкими. Вывод следует.

// file: f2i.cpp
//
// compiled with MinGW x86 (gcc version 4.6.2) as:
//   g++ -Wall -O2 -std=c++03 f2i.cpp -o f2i.exe
#include <iostream>
#include <iomanip>
#include <limits>

using namespace std;

template<class I, class F> I truncAndCap(F f)
{
/*
  This function converts (by truncating the
  fractional part) the floating-point value f (of type F)
  into an integer value (of type I), avoiding undefined
  behavior by returning std::numeric_limits<I>::min() and
  std::numeric_limits<I>::max() when f is too small or
  too big to be converted to type I directly.

  2 problems:
  - F may fail to convert to I,
    which is undefined behavior and we want to avoid that.
  - I may not convert exactly into F
    - Direct I & F comparison fails because of I to F promotion,
      which can be inexact.

  This solution is for the most practical case when I and F
  are radix-2 (binary) integer and floating-point types.
*/
  int Idigits = numeric_limits<I>::digits;
  int Isigned = numeric_limits<I>::is_signed;

/*
  Calculate cutOffMax = 2 ^ std::numeric_limits<I>::digits
  (where ^ denotes exponentiation) as a value of type F.

  We assume that F is a radix-2 (binary) floating-point type AND
  it has a big enough exponent part to hold the value of
  std::numeric_limits<I>::digits.

  FLT_MAX_10_EXP/DBL_MAX_10_EXP/LDBL_MAX_10_EXP >= 37
  (guaranteed per C++ standard from 2003/C standard from 1999)
  corresponds to log2(1e37) ~= 122, so the type I can contain
  up to 122 bits. In practice, integers longer than 64 bits
  are extremely rare (if existent at all), especially on old systems
  of the 2003 C++ standard's time.
*/
  const F cutOffMax = F(I(1) << Idigits / 2) * F(I(1) << (Idigits / 2 + Idigits % 2));

  if (f >= cutOffMax)
    return numeric_limits<I>::max();

/*
  Calculate cutOffMin = - 2 ^ std::numeric_limits<I>::digits
  (where ^ denotes exponentiation) as a value of type F for
  signed I's OR cutOffMin = 0 for unsigned I's in a similar fashion.
*/
  const F cutOffMin = Isigned ? -F(I(1) << Idigits / 2) * F(I(1) << (Idigits / 2 + Idigits % 2)) : 0;

  if (f <= cutOffMin)
    return numeric_limits<I>::min();

/*
  Mathematically, we may still have a little problem (2 cases):
    cutOffMin < f < std::numeric_limits<I>::min()
    srd::numeric_limits<I>::max() < f < cutOffMax

  These cases are only possible when f isn't a whole number, when
  it's either std::numeric_limits<I>::min() - value in the range (0,1)
  or std::numeric_limits<I>::max() + value in the range (0,1).

  We can ignore this altogether because converting f to type I is
  guaranteed to truncate the fractional part off, and therefore
  I(f) will always be in the range
  [std::numeric_limits<I>::min(), std::numeric_limits<I>::max()].
*/

  return I(f);
}

template<class I, class F> void test(const char* msg, F f)
{
  I i = truncAndCap<I,F>(f);
  cout <<
    msg <<
    setiosflags(ios_base::showpos) <<
    setw(14) << setprecision(12) <<
    f << " -> " <<
    i <<
    resetiosflags(ios_base::showpos) <<
    endl;
}

#define TEST(I,F,VAL) \
  test<I,F>(#F " -> " #I ": ", VAL);

int main()
{
  TEST(short, float,     -1.75f);
  TEST(short, float,     -1.25f);
  TEST(short, float,     +0.00f);
  TEST(short, float,     +1.25f);
  TEST(short, float,     +1.75f);

  TEST(short, float, -32769.00f);
  TEST(short, float, -32768.50f);
  TEST(short, float, -32768.00f);
  TEST(short, float, -32767.75f);
  TEST(short, float, -32767.25f);
  TEST(short, float, -32767.00f);
  TEST(short, float, -32766.00f);
  TEST(short, float, +32766.00f);
  TEST(short, float, +32767.00f);
  TEST(short, float, +32767.25f);
  TEST(short, float, +32767.75f);
  TEST(short, float, +32768.00f);
  TEST(short, float, +32768.50f);
  TEST(short, float, +32769.00f);

  TEST(int, float, -2147483904.00f);
  TEST(int, float, -2147483648.00f);
  TEST(int, float, -16777218.00f);
  TEST(int, float, -16777216.00f);
  TEST(int, float, -16777215.00f);
  TEST(int, float, +16777215.00f);
  TEST(int, float, +16777216.00f);
  TEST(int, float, +16777218.00f);
  TEST(int, float, +2147483648.00f);
  TEST(int, float, +2147483904.00f);

  TEST(int, double, -2147483649.00);
  TEST(int, double, -2147483648.00);
  TEST(int, double, -2147483647.75);
  TEST(int, double, -2147483647.25);
  TEST(int, double, -2147483647.00);
  TEST(int, double, +2147483647.00);
  TEST(int, double, +2147483647.25);
  TEST(int, double, +2147483647.75);
  TEST(int, double, +2147483648.00);
  TEST(int, double, +2147483649.00);

  TEST(unsigned, double,          -1.00);
  TEST(unsigned, double,          +1.00);
  TEST(unsigned, double, +4294967295.00);
  TEST(unsigned, double, +4294967295.25);
  TEST(unsigned, double, +4294967295.75);
  TEST(unsigned, double, +4294967296.00);
  TEST(unsigned, double, +4294967297.00);

  return 0;
}

Вывод (ideone печатает так же, как мой компьютер):

float -> short:          -1.75 -> -1
float -> short:          -1.25 -> -1
float -> short:             +0 -> +0
float -> short:          +1.25 -> +1
float -> short:          +1.75 -> +1
float -> short:         -32769 -> -32768
float -> short:       -32768.5 -> -32768
float -> short:         -32768 -> -32768
float -> short:      -32767.75 -> -32767
float -> short:      -32767.25 -> -32767
float -> short:         -32767 -> -32767
float -> short:         -32766 -> -32766
float -> short:         +32766 -> +32766
float -> short:         +32767 -> +32767
float -> short:      +32767.25 -> +32767
float -> short:      +32767.75 -> +32767
float -> short:         +32768 -> +32767
float -> short:       +32768.5 -> +32767
float -> short:         +32769 -> +32767
float -> int:    -2147483904 -> -2147483648
float -> int:    -2147483648 -> -2147483648
float -> int:      -16777218 -> -16777218
float -> int:      -16777216 -> -16777216
float -> int:      -16777215 -> -16777215
float -> int:      +16777215 -> +16777215
float -> int:      +16777216 -> +16777216
float -> int:      +16777218 -> +16777218
float -> int:    +2147483648 -> +2147483647
float -> int:    +2147483904 -> +2147483647
double -> int:    -2147483649 -> -2147483648
double -> int:    -2147483648 -> -2147483648
double -> int: -2147483647.75 -> -2147483647
double -> int: -2147483647.25 -> -2147483647
double -> int:    -2147483647 -> -2147483647
double -> int:    +2147483647 -> +2147483647
double -> int: +2147483647.25 -> +2147483647
double -> int: +2147483647.75 -> +2147483647
double -> int:    +2147483648 -> +2147483647
double -> int:    +2147483649 -> +2147483647
double -> unsigned:             -1 -> 0
double -> unsigned:             +1 -> 1
double -> unsigned:    +4294967295 -> 4294967295
double -> unsigned: +4294967295.25 -> 4294967295
double -> unsigned: +4294967295.75 -> 4294967295
double -> unsigned:    +4294967296 -> 4294967295
double -> unsigned:    +4294967297 -> 4294967295
person Alexey Frunze    schedule 27.09.2012
comment
Я беру этот [комментарий] обратно. Выход сборки выглядит разумным. Сначала компилятор подстроил вызов truncAndCap(), поэтому рядом с кодом конвертации я увидел много ненужного (связанного с std::cout). Добавление -fno-inline показало, что truncAndCap() было коротким. - person Alexey Frunze; 27.09.2012