Может ли преобразование из double в int быть записано в переносном C - программирование
Подтвердить что ты не робот

Может ли преобразование из double в int быть записано в переносном C

Мне нужно написать такую функцию, как double_to_int(double val, int *err) которая могла бы скрывать двойной val до целого числа, когда это возможно; в противном случае сообщите об ошибке (NAN/INFs/OUT_OF_RANGE).

поэтому реализация псевдокода будет выглядеть так:

if isnan(val):
    err = ERR_NAN
    return 0
if val < MAX_INT:
    err = ERR_MINUS_INF
    return MIN_INT
if ...
return (int)val

На SO есть как минимум два похожих вопроса: в этом ответе он решался достаточно чистым способом, хотя это решение C++ - в C у нас нет переносных цифр для подписанного int. В этом ответе он объяснил, почему мы не можем просто проверить (val > INT_MAX || val < INT_MIN).

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

Поэтому мой вопрос: есть ли способ реализовать функцию double_to_int кросс-платформенным способом (основываясь только на стандарте C, даже не рассматривая целевые платформы для поддержки IEEE-754).

4b9b3361

Ответ 1

[Этот ответ был отредактирован с совершенно новым подходом.]

Этот подход использует определение форматов с плавающей запятой в стандарте C - как подписанную базовую цифру b, умноженную на силу b. Знание числа цифр в значении (предоставлено DBL_MANT_DIG) и пределе экспоненты (предоставлено DBL_MAX_EXP) позволяет нам подготовить точные double значения в качестве конечных точек.

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

/*  This code demonstrates safe conversion of double to int in which the
    input double is converted to int if and only if it is in the supported
    domain for such conversions (the open interval (INT_MIN-1, INT_MAX+1)).
    If the input is not in range, an error is indicated (by way of an
    auxiliary argument) and no conversion is performed, so all behavior is
    defined.

    There are a few requirements not fully covered by the C standard.  They
    should be uncontroversial and supported by all reasonable C implementations:

        Conversion of an int that is representable in double produces the
        exact value.

        The following operations are exact in floating-point:

            Dividing by the radix of the floating-point format, within its
            range.

            Multiplying by +1 or -1.

            Adding or subtracting two values whose sum or difference is
            representable.

        FLT_RADIX is representable in int.

        DBL_MIN_EXP is not greater than -DBL_MANT_DIG.  (The code can be
        modified to eliminate this requirement.)

    Deviations from the requested routine include:

        This code names the routine DoubleToInt instead of double_to_int.

        The only error indicated is ERANGE.  Code to distinguish the error more
        finely, such as providing separate values for NaNs, infinities, and
        out-of-range finite values, could easily be added.
*/


#include <float.h>
#include <errno.h>
#include <limits.h>
#include <stdio.h>


/*  These values will be initialized to the greatest double value not greater
    than INT_MAX+1 and the least double value not less than INT_MIN-1.
*/
static double UpperBound, LowerBound;


/*  Return the double of the same sign of x that has the greatest magnitude
    less than x+s, where s is -1 or +1 according to whether x is negative or
    positive.
*/
static double BiggestDouble(int x)
{
    /*  All references to "digits" in this routine refer to digits in base
        FLT_RADIX.  For example, in base 3, 77 would have four digits (2212).

        In this routine, "bigger" and "smaller" refer to magnitude.  (3 is
        greater than -4, but -4 is bigger than 3.)
    */

    //  Determine the sign.
    int s = 0 < x ? +1 : -1;

    //  Count how many digits x has.
    int digits = 0;
    for (int t = x; t; ++digits)
        t /= FLT_RADIX;

    /*  If the double type cannot represent finite numbers this big, return the
        biggest finite number it can hold, with the desired sign.
    */
    if (DBL_MAX_EXP < digits)
        return s*DBL_MAX;

    //  Determine whether x is exactly representable in double.
    if (DBL_MANT_DIG < digits)
    {
        /*  x is not representable, so we will return the next lower
            representable value by removing just as many low digits as
            necessary.  Note that x+s might be representable, but we want to
            return the biggest double less than it, which is also the biggest
            double less than x.
        */

        /*  Figure out how many digits we have to remove to leave at most
            DBL_MANT_DIG digits.
        */
        digits = digits - DBL_MANT_DIG;

        //  Calculate FLT_RADIX to the power of digits.
        int t = 1;
        while (digits--) t *= FLT_RADIX;

        return x / t * t;
    }
    else
    {
        /*  x is representable.  To return the biggest double smaller than
            x+s, we will fill the remaining digits with FLT_RADIX-1.
        */

        //  Figure out how many additional digits double can hold.
        digits = DBL_MANT_DIG - digits;

        /*  Put a 1 in the lowest available digit, then subtract from 1 to set
            each digit to FLT_RADIX-1.  (For example, 1 - .001 = .999.)
        */
        double t = 1;
        while (digits--) t /= FLT_RADIX;
        t = 1-t;

        //  Return the biggest double smaller than x+s.
        return x + s*t;
    }
}


/*  Set up supporting data for DoubleToInt.  This should be called once prior
    to any call to DoubleToInt.
*/
static void InitializeDoubleToInt(void)
{
    UpperBound = BiggestDouble(INT_MAX);
    LowerBound = BiggestDouble(INT_MIN);
}


/*  Perform the conversion.  If the conversion is possible, return the
    converted value and set *error to zero.  Otherwise, return zero and set
    *error to ERANGE.
*/
static int DoubleToInt(double x, int *error)
{
    if (LowerBound <= x && x <= UpperBound)
    {
        *error = 0;
        return x;
    }
    else
    {
        *error = ERANGE;
        return 0;
    }
}


#include <string.h>


static void Test(double x)
{
    int error, y;
    y = DoubleToInt(x, &error);
    printf("%.99g -> %d, %s.\n", x, y, error ? strerror(error) : "No error");
}


#include <math.h>


int main(void)
{
    InitializeDoubleToInt();
    printf("UpperBound = %.99g\n", UpperBound);
    printf("LowerBound = %.99g\n", LowerBound);

    Test(0);
    Test(0x1p31);
    Test(nexttoward(0x1p31, 0));
    Test(-0x1p31-1);
    Test(nexttoward(-0x1p31-1, 0));
}

Ответ 2

Ответ на "Может ли разговор с двойного на int быть написанным на портативном C" явно "да".

Например, вы можете спрятать плавающее значение в строке, провести проверку на основе строк (т.е. Путем сопоставления по строкам с максимальными и минимальными значениями, которые вы также sprintfd), проверки, округления и т.д., А затем sscanf для известной допустимой строки для конечное значение.

По сути, вы должны двигаться к промежуточному представлению, которое (а) переносится и (б) удобно. Строки C отлично переносимы, но не очень удобны. Если вы можете использовать внешние библиотеки, есть несколько удобных, но мобильность которых должна быть подтверждена.

Например (опускает округление):

#include <stdio.h>
#include <math.h>
#include <limits.h>
#include <string.h>

int convert(double inVal) {
    // basic range check - does anybody have an integer format with more than 300 bits?
    if (fabs(inVal) > 1.0E100) {
        printf("well out of range");
        return 1;
    }

    // load string buffer with input
    char buf[110];
    sprintf(buf, "%0105.0f", inVal);

    // do range check on strings
    if (inVal < 0) {
        char minVal[110];
        sprintf(minVal, "%0105d", INT_MIN);
        if (strcmp(buf, minVal) > 0) {
            printf("too small input: %f\n", inVal);
            return -1;  // needs better error signify
        }
    } else {
        char maxVal[110];
        sprintf(maxVal, "%0105d", INT_MAX);
        if (strcmp(maxVal, buf) < 0) {
            printf("too large input: %f\n", inVal);
            return -1;  // needs better error signify
        }
    }

    // do final conversion
    int result;
    sscanf(buf, "%d", &result);

    printf("input: %f result: %d\n", inVal, result);  // diagnostic

    return result;
}

int main()
{
    // test values    
    convert( 0.);
    convert( -123.5);
    convert( 123.5);

    convert( ((double)INT_MIN)-1);
    convert( ((double)INT_MIN));
    convert( ((double)INT_MIN)+1);
    convert( 2.0*((double)INT_MIN));
    convert( ((double)INT_MIN)/2);

    convert( ((double)INT_MAX)-1);
    convert( ((double)INT_MAX));
    convert( ((double)INT_MAX)+1);
    convert( 2.0*((double)INT_MAX));
    convert( ((double)INT_MAX)/2);

    return 0;
}

Что дает ожидаемые преобразования (см. Тестовые примеры в конце выше):

% gcc test.c ; ./a.out
input: 0.000000 result: 0
input: -123.500000 result: -124
input: 123.500000 result: 124
too small input: -2147483649.000000
input: -2147483648.000000 result: -2147483648
input: -2147483647.000000 result: -2147483647
too small input: -4294967296.000000
input: -1073741824.000000 result: -1073741824
input: 2147483646.000000 result: 2147483646
input: 2147483647.000000 result: 2147483647
too large input: 2147483648.000000
too large input: 4294967294.000000
input: 1073741823.500000 result: 1073741824

Ответ 3

(Этот ответ спорит, хотя я все еще думаю, что я прав, поэтому, пожалуйста, не выдвигайте неразумно.)

Вы не можете реализовать такую функцию в переносном C.

В этом отношении он скорее напоминает malloc & c.

Мораль этой истории на самом деле заключается в том, что типы смешивания никогда не являются хорошей идеей в C; т.е. писать код таким образом, чтобы преобразования типов не требовались.

Ответ 4

Может ли преобразование из double в int быть записано в переносном C (?)

есть ли способ реализовать функцию double_to_int кросс-платформенным способом (основываясь только на стандарте C, даже не рассматривая целевые платформы для поддержки IEEE-754).

int double_to_int(double val, int *err)

Деталь: (int)val обрезает дробную часть, поэтому диапазон конвертируемого val с использованием (int)val математически:
INT_MIN - 0.9999... ≤ val ≤ INT_MAX + 0.9999... или
INT_MIN - 1 < val < INT_MAX + 1.


Да, кросс-платформенный способ, используя точную математику и константу с плавающей запятой, код может проверить успех конверсии.

2.0*(INT_MAX/2+1), безусловно, точно преобразуется в константу FP.

val - INT_MIN > -1.0 сродни val > INT_MIN - 1.0 но не претерпевает неточности (с обычными машинами с INT_MIN - 1.0 дополнениями) с INT_MIN - 1.0. Напомним, что целочисленный тип может иметь большую точность, чем double. Рассмотрим 64-битные int и INT_MIN - 1.0 не представляются как double.

Код не использует (double)INT_MAX который также может быть неточным.


Чтобы скопировать себя:

#include <limits.h>
#define DBL_INT_MAXP1 (2.0*(INT_MAX/2+1)) 
#define DBL_INT_MINM1 (2.0*(INT_MIN/2-1)) 

int double_to_int(double val, int *err) {
  if (val < DBL_INT_MAXP1) {
    #if -INT_MAX == INT_MIN
    // rare non-2 complement machine 
    if (val > DBL_INT_MINM1) {
      *err = OK;
      return (int) val;
    }
    #else
    if (val - INT_MIN > -1.0) {
      *err = OK;
      return (int) val;
    }
    #endif 
    // Underflow
    *err = ERR_MINUS_INF;
    return INT_MIN;
  }
  if (x > 0) {
    // Overflow
    *err = ERR_PLUS_INF;
    return INT_MAX;
  }
  // NaN;
  *err = ERR_NAN;
  return 0;
}

Угловая слабость: FLT == 10 и целочисленный тип> 34 бит.

Ответ 5

Возможно, это может сработать:

#define BYTES_TO_BITS(x)    (x*8)

void numToIntnt(double num, int *output) {
    const int upperLimit = ldexp(1.0, (BYTES_TO_BITS(sizeof(int))-1))-1;
    const int lowerLimit = (-1)*ldexp(1.0, (BYTES_TO_BITS(sizeof(int))-1));

    /*
     * or a faster approach if the rounding is acceptable:
     * const int upperLimit = ~(1<<(BYTES_TO_BITS(sizeof(int))-1));
     * const int lowerLimit = (1<<(BYTES_TO_BITS(sizeof(int))-1));
     */

    if(num > upperLimit) {
        /* report invalid conversion */
    } else if (num < lowerLimit) {
        /* report invalid conversion */
    } else {
        *output = (int)num;
    }
}                                                                                                                          

Ответ 6

Основная проблема заключается в том, чтобы найти min_double_to_int и max_double_to_int, самый маленький и самый большой double, соответственно, которые могут быть преобразованы в int.

Сама переносимая функция преобразования может быть записана на C11 как

int double_to_int(const double value, int *err)
{
    if (!isfinite(value)) {
        if (isnan(value)) {
            if (err) *err = ERR_NAN;
            return 0;
        } else
        if (signbit(value)) {
            if (err) *err = ERR_NEG_INF;
            return INT_MIN;
        } else {
            if (err) *err = ERR_POS_INF;
            return INT_MAX;
        }
    }

    if (value < min_double_to_int) {
        if (err) *err = ERR_TOOSMALL;
        return INT_MIN;
    } else
    if (value > max_double_to_int) {
        if (err) *err = ERR_TOOLARGE;
        return INT_MAX;
    }

    if (err) *err = 0;
    return (int)value;
}

Прежде чем использовать вышеприведенную функцию, нам нужно назначить min_double_to_int и max_double_to_int.

EDITED по 2018-07-03: Пересмотренный подход.

Мы можем использовать простую функцию, чтобы найти наименьшую мощность в десять, которая по меньшей мере INT_MAX как INT_MAX/INT_MIN. Если они меньше DBL_MAX_10_EXP, диапазон double больше, чем диапазон int, и мы можем сделать INT_MAX и INT_MIN double.

В противном случае мы построим строку, содержащую десятичное представление INT_MAX/INT_MIN, и используем strtod() чтобы преобразовать их в double. Если эта операция переполняется, это означает, что диапазон double меньше, чем диапазон int, и мы можем использовать DBL_MAX/-DBL_MAX как max_double_to_int и min_double_to_int соответственно.

Когда у нас есть INT_MAX как double, мы можем использовать цикл для увеличения этого значения с помощью nextafter(value, HUGE_VAL). Наибольшее значение, которое является конечным и округленное с помощью floor() прежнему дает одинаковое double значение, max_double_to_int.

Аналогично, когда мы имеем INT_MIN как double, мы можем использовать цикл для уменьшения этого значения с помощью nextafter(value, -HUGE_VAL). Наибольшее значение по величине, которое все еще является конечным, и округляет (ceil()) до одного и того же double, равно min_double_to_int.

Вот пример программы, чтобы проиллюстрировать это:

#include <stdlib.h>
#include <limits.h>
#include <string.h>
#include <float.h>
#include <stdio.h>
#include <errno.h>
#include <math.h>

static double  max_double_to_int = -1.0;
static double  min_double_to_int = +1.0;

#define  ERR_OK        0
#define  ERR_NEG_INF  -1
#define  ERR_POS_INF  -2
#define  ERR_NAN      -3
#define  ERR_NEG_OVER  1
#define  ERR_POS_OVER  2

int double_to_int(const double value, int *err)
{
    if (!isfinite(value)) {
        if (isnan(value)) {
            if (err) *err = ERR_NAN;
            return 0;
        } else
        if (signbit(value)) {
            if (err) *err = ERR_NEG_INF;
            return INT_MIN;
        } else {
            if (err) *err = ERR_POS_INF;
            return INT_MAX;
        }
    }

    if (value < min_double_to_int) {
        if (err) *err = ERR_NEG_OVER;
        return INT_MIN;
    } else
    if (value > max_double_to_int) {
        if (err) *err = ERR_POS_OVER;
        return INT_MAX;
    }

    if (err) *err = ERR_OK;
    return (int)value;
}


static inline double  find_double_max(const double  target)
{
    double  next = target;
    double  curr;

    do {
        curr = next;
        next = nextafter(next, HUGE_VAL);
    } while (isfinite(next) && floor(next) == target);

    return curr;
}


static inline double  find_double_min(const double  target)
{
    double  next = target;
    double  curr;

    do {
        curr = next;
        next = nextafter(next, -HUGE_VAL);
    } while (isfinite(next) && ceil(next) == target);

    return curr;
}


static inline int  ceil_log10_abs(int  value)
{
    int  result = 1;

    while (value < -9 || value > 9) {
        result++;
        value /= 10;
    }

    return result;
}


static char *int_string(const int value)
{
    char    *buf;
    size_t   max = ceil_log10_abs(value) + 4;
    int      len;

    while (1) {
        buf = malloc(max);
        if (!buf)
            return NULL;

        len = snprintf(buf, max, "%d", value);
        if (len < 1) {
            free(buf);
            return NULL;
        }

        if ((size_t)len < max)
            return buf;

        free(buf);
        max = (size_t)len + 2;
    }
}

static int int_to_double(double *to, const int ivalue)
{
    char   *ival, *iend;
    double  dval;

    ival = int_string(ivalue);
    if (!ival)
        return -1;

    iend = ival;
    errno = 0;
    dval = strtod(ival, &iend);
    if (errno == ERANGE) {
        if (*iend != '\0' || dval != 0.0) {
            /* Overflow */
            free(ival);
            return +1;
        }
    } else
    if (errno != 0) {
        /* Unknown error, not overflow */
        free(ival);
        return -1;
    } else
    if (*iend != '\0') {
        /* Overflow */
        free(ival);
        return +1;
    }
    free(ival);

    /* Paranoid overflow check. */
    if (!isfinite(dval))
        return +1;

    if (to)
        *to = dval;

    return 0;
}

int init_double_to_int(void)
{
    double  target;

    if (DBL_MAX_10_EXP > ceil_log10_abs(INT_MAX))
        target = INT_MAX;
    else {
        switch (int_to_double(&target, INT_MAX)) {
        case 0:  break;
        case 1:  target = DBL_MAX; break;
        default: return -1;
        }
    }

    max_double_to_int = find_double_max(target);

    if (DBL_MAX_10_EXP > ceil_log10_abs(INT_MIN))
        target = INT_MIN;
    else {
        switch (int_to_double(&target, INT_MIN)) {
        case 0:  break;
        case 1:  target = -DBL_MAX; break;
        default: return -1;
        }
    }

    min_double_to_int = find_double_min(target);

    return 0;
}

int main(void)
{
    int     i, val, err;
    double  temp;

    if (init_double_to_int()) {
        fprintf(stderr, "init_double_to_int() failed.\n");
        return EXIT_FAILURE;
    }

    printf("(int)max_double_to_int = %d\n", (int)max_double_to_int);
    printf("(int)min_double_to_int = %d\n", (int)min_double_to_int);
    printf("max_double_to_int = %.16f = %a\n", max_double_to_int, max_double_to_int);
    printf("min_double_to_int = %.16f = %a\n", min_double_to_int, min_double_to_int);

    temp = nextafter(max_double_to_int, 0.0);
    for (i = -1; i <= 1; i++) {
        val = double_to_int(temp, &err);
        printf("(int)(max_double_to_int %+d ULP)", i);
        switch (err) {
        case ERR_OK:       printf(" -> %d\n", val); break;
        case ERR_POS_OVER: printf(" -> overflow\n"); break;
        case ERR_POS_INF:  printf(" -> infinity\n"); break;
        default:           printf(" -> BUG\n");
        }
        temp = nextafter(temp, HUGE_VAL);
    }

    temp = nextafter(min_double_to_int, 0.0);
    for (i = 1; i >= -1; i--) {
        val = double_to_int(temp, &err);
        printf("(int)(min_double_to_int %+d ULP)", i);
        switch (err) {
        case ERR_OK:       printf(" -> %d\n", val); break;
        case ERR_NEG_OVER: printf(" -> overflow\n"); break;
        case ERR_NEG_INF:  printf(" -> infinity\n"); break;
        default:           printf(" -> BUG\n");
        }
        temp = nextafter(temp, -HUGE_VAL);
    }

    return EXIT_SUCCESS;
}

Ответ 7

Да. (обработка наном/крыльями опущена для краткости)

int convert(double x) {
   if (x == INT_MAX) {
     return INT_MAX;
   } else if (x > INT_MAX) {
     err = ERR_OUT_OF_RANGE; 
     return INT_MAX;
   } else if (x == INT_MIN) {
     return INT_MIN;
   } else if (x < INT_MIN)
     err = ERR_OUT_OF_RANGE;
     return INT_MIN;
   } else {
     return x;
   }
}

Объяснение.

INT_MAX случаи, как объясняется в одном из связанных ответов, - это когда INT_MAX не представляется как double точно и округляется при преобразовании в double, а симметричный случай - с INT_MIN. Тот случай, когда if (x > INT_MAX) терпит неудачу. То есть сравнение возвращает false, но мы по-прежнему не можем напрямую преобразовать x в int.

То, что связанный ответ не распознается, состоит в том, что существует только один двойной номер, который не (double)INT_MAX тест, а именно (double)INT_MAX, и мы можем легко поймать этот случай, явно проверив x == INT_MAX.

Изменить Как отмечено в комментариях, это может завершиться неудачно, если INT_MAX или INT_MIN находятся за пределами диапазона double. Хотя это маловероятно, это не исключает стандарт. В такой реализации преобразование является просто (int)x. Должна быть проще обнаружить такую реализацию во время конфигурации, чем во время выполнения. Если последнее абсолютно необходимо, можно выполнить эту операцию один раз:

static int need_simple_conversion = 0;
char* str = malloc(sizeof(int)*CHAR_BIT+1);
sprintf (str, "%d", INT_MAX);
errno = 0;
if (strtod(s, NULL) == HUGE_VAL && errno == ERANGE) {
   // INT_MAX overflows double => double can never overflow int
   need_simple_conversion = 1;
}

затем

if (need_simple_conversion)
    return x;
else { // as above

Для параноика между нами сделайте это с помощью INT_MIN и выполните проверку отдельно для положительных и отрицательных удвоений.

Ответ 8

Насколько я могу судить, основная проблема заключается в следующем: double-> int-> double identity для значений INT_MAX и INT_MIN. Интересно, что C имеет способ выразить это:

int isok(int val) {
   double dv = val;
   int iv = dv;
   return val == iv;
}

Из этого можно работать очень сжатую форму вышеприведенных ответов, так как вы можете использовать это, чтобы определить, насколько INT_MAX, INT_MIN достаточно сопоставимы, таким образом:

if (isok(INT_MAX) && isok(INT_MIN) && f >= INT_MIN && f < INT_MAX) {
     // do your weirdo float stuff here...
}

но, конечно же, полагаясь на строгую систему преобразования Cs, вы получаете бесплатную лицензию компилятора для переформатирования вашего диска, поэтому, возможно, вместо этого вы можете использовать printf/scanf.