Как я могу доверять приведению от двойного к целому?

Я работал над простым кодом для создания гистограмм и нашел следующий код:

double value = 1.2;
double bucketSize = 0.4;
double bucketId = value / bucketSize;

std::cout << "bucketId as double: " << bucketId << std::endl;
std::cout << "bucketId as int: " << int(bucketId) << std::endl;

приводит к сумасшедшему выводу:

bucketId as double: 3
bucketId as int: 2

что в основном разрушает мое доверие к компьютерам ;) при поиске правильного bucketId для value при создании гистограммы.

Я знаю, что есть ошибки округления и т. д., но есть ли какое-нибудь общее решение этой проблемы?

(На всякий случай) Пожалуйста, не предлагайте добавлять 0.5 к результату деления перед приведением к int, поскольку, по-видимому, в некоторых случаях это не очень хорошо работает (например, double value = 3; double bucketSize = 2;)

Заранее спасибо.


person Moomin    schedule 21.10.2014    source источник
comment
Какими хотите быть 3/2?   -  person n. 1.8e9-where's-my-share m.    schedule 21.10.2014
comment
Целая часть результата, поэтому 3 для 1,2/0,4 и 1 для 3/2. Поэтому std::round() не поможет.   -  person Moomin    schedule 21.10.2014
comment
что в основном разрушает мое доверие к компьютерам - да, плавающая точка имеет тенденцию иметь такой эффект.   -  person Fred Larson    schedule 21.10.2014
comment
Сначала выведите 1,2 и 0,4 с 20 знаками после запятой. Возможно, вас ждут интересные открытия. Затем прочитайте floating-point-gui.de.   -  person n. 1.8e9-where's-my-share m.    schedule 21.10.2014
comment
Добавление 0.5 может привести к неявным ошибкам, см. мой ответ здесь для примера ближе к середине.   -  person Shafik Yaghmour    schedule 21.10.2014


Ответы (8)


В комментариях вы говорите, что хотите Integer part of the result. Что ж, к сожалению, результатом double 1.2 / 0.4 оказывается 2.9999999999999996 (на моей машине вы можете увидеть точное значение cout, используя std::setprecision), и поэтому целая часть результата равна 2. Это, как вы знаете, связано с тем, что не все числа могут быть представлены числами с плавающей запятой, и потому, что операции с плавающей запятой вызывают ошибки.

Взятие целой части числа с плавающей запятой находится на том же уровне, что и сравнение чисел с плавающей запятой с равенством; Вы не получите стабильных результатов. Если вам нужны точные результаты, общее решение состоит в том, чтобы вообще не использовать числа с плавающей запятой, а вместо этого использовать числа с фиксированной запятой.

Как и в случае сравнения на равенство, вы можете обойти эту проблему с помощью соответствующего значения epsilon. В этом случае вы можете добавить (или вычесть, если отрицательный) очень маленькое число с плавающей запятой к результату, прежде чем брать целую часть. Добавленное число должно быть больше, чем наибольшая возможная ошибка, которую может иметь число, но меньше, чем наименьшее число точности, которое вы должны поддерживать (чтобы 9,999 не стало 10, если вы должны поддерживать точность до 0,001). Вычислить хорошее число для этого может быть довольно сложно.

person eerorika    schedule 21.10.2014
comment
Моя беда - под результатом я имел в виду то, что получается из математики, а не из деления, выполненного машиной :). - person Moomin; 21.10.2014
comment
@Moomin Да, я предполагал это в своем ответе. - person eerorika; 21.10.2014
comment
Спасибо за Ваш ответ. Я обнаружил, что приведение к float перед приведением к int дает здесь некоторую помощь (моя ситуация), но также не является общим решением. - person Moomin; 21.10.2014
comment
@Moomin Это будет зависеть от фактических значений. Преобразование результатов в float может помочь, но с другими значениями это может дать неправильный результат, в то время как вы получили бы правильный результат без приведения. - person James Kanze; 21.10.2014

Я основываю это более или менее на некоторых ваших комментариях другим. Чтобы получить целую часть, решение состоит в том, чтобы использовать modf. Но целая часть 1.2 / 0.4 вполне может быть 2, а не 3; 0.4 не может быть представлено в машинных числах с плавающей запятой (по крайней мере, большинство из них), поэтому вы делите на что-то очень близкое к 0.4.

Главный вопрос в том, чего вы на самом деле хотите. Если вы хотите дискреционно (существует ли такое слово) в зависимости от bucketSize, то правильный способ сделать это - использовать масштабированные целые числа повсюду:

int value = 12;
int bucketSize = 4;
int bucketId = value / bucketSize;

а потом:

std::cout << "bucketId as double: " << bucketId / 10.0 << std::endl;
std::cout << "bucketId as int: " << bucketId / 10 << std::endl;

В противном случае, если вы хотите сохранить значения как двойные, вам придется решить, насколько близко близко для преобразования в int, а затем использовать свою собственную функцию:

int
asInt( double d )
{
    double results;
    double frac = modf( d, &results );
    if ( frac > 1.0 - yourEpsilonHere ) {
        results += 1.0;
    }
    return results;
}

Вам решать, какое значение подходит для yourEpsilonHere; это зависит от приложения. (Один раз, когда я использовал эту технику, мы использовали 1E-9. Однако это не означает, что она подходит для вас.)

person James Kanze    schedule 21.10.2014

Используйте 1_. Он возвращает ближайшее целое число к вашему двойному числу.

#include<numeric>
double value = 1.2;
double bucketSize = 0.4;
double bucketId = value / bucketSize;

std::cout << "bucketId as int: " << std::lround(bucketId) << std::endl;

Обратите внимание, что 3.0/2.0 по-прежнему может привести к неожиданным результатам, в зависимости от того, является ли результат 1.4999998 или 1.5000001, наивно произнесенным.

person davidhigh    schedule 21.10.2014
comment
Ну проблема в том, что не нужен ближайший - мне нужна целая часть числа. Для результата 1.6 lround даст мне 2 вместо ожидаемого 1. - person Moomin; 21.10.2014
comment
Согласно вашему описанию, вам нужен ближайший. В противном случае вы должны использовать (int) (1.2/0.4), так как это точно извлекает целую часть вашего числа ... которая равна 2 на этой машине и 3 на другой. - person davidhigh; 21.10.2014
comment
Чтобы получить целую часть, используйте modf. Проблема в том, что вам действительно не нужна целая часть, потому что целая часть 1.2 / 0.4 равна 2, по крайней мере, на вашей машине. - person James Kanze; 21.10.2014

Может быть, фиксированная десятичная аппроксимация?

(int)(value * 100)/(int)(bucketSize *100)
person Grady Player    schedule 21.10.2014

Если вы хотите, чтобы 0,25 перед возвратом следующего числа (например, от (double)1.75 до (int)2) использовали int(floor(buckedId+0.25)).

Дело в том, насколько сильно вы хотите округлить до предыдущего числа.

person KugBuBu    schedule 21.10.2014

#include<iostream>
#include <limits>
int main()
{
    double value = 1.200000000000000;
    double bucketSize = 0.4000000000000000;
    double bucketId = value / bucketSize;
    std::cout.precision(16);
    std::cout << "bucketId as double: " <<  std::fixed << bucketId << std::endl;
    std::cout << "bucketId as int: " << int(bucketId) << std::endl;
    return 1;
}

попробуйте это в своей системе, вы хотели бы что-то вроде

ведроId как двойной: 2.9999999999999996

ведроId как целое: 2

и чем с

std::cout.precision(15);

ты бы

ведроId как двойной: 3.000000000000000

ведроId как целое: 2

это происходит из-за того, что предел точности double равен 15 , вы также можете попробовать спроектировать с помощью long double и изменить точность.

person Hummingbird    schedule 21.10.2014
comment
Для начала: double bucketSize = 0.4;, а затем выведите bucketSize с очень большой точностью. (Хотя ваша реализация может не поддерживать достаточно большую точность, чтобы оно не отображалось как 0.4. Но поскольку 0.4 не может быть представлено в double, это определенно не его фактическое значение.) - person James Kanze; 21.10.2014
comment
@JamesKanze, да .. вы правы .. но почему точность 15, которая была бы двойной, дает мне 3,0000000000000000, а более высокая дает мне 2,9 ... я полагаю, что OP сталкивается с той же проблемой. я сам хотел бы научиться. Я подумал, что это может помочь ему. - person Hummingbird; 21.10.2014
comment
Потому что 2.9..., округленное до 15 цифр, равно 3.0? Используя g++ (который поддерживает вывод с очень высокой точностью), я получаю value == 1.19999999999999995559, size == 0.40000000000000002220 и id == 2.99999999999999955591, когда вывожу его значения с точностью до 20 цифр. С 15 или менее цифрами все округляется до его начальных значений. - person James Kanze; 21.10.2014
comment
И предел точности double обычно составляет 17 десятичных цифр, а не 15. Хотя это зависит от того, что именно вы подразумеваете под точностью: если вы хотите вывести значение и гарантировать получение того же значения обратно, когда вы его читаете, вам нужно 17 цифр. - person James Kanze; 21.10.2014
comment
я сослался на это stackoverflow.com/questions/9999221/ - person Hummingbird; 21.10.2014
comment
Ответы там на самом деле неплохие. Они указывают на различные значения десятичной точности. По сути, если у вас есть десятичное значение, преобразуйте его в двойное, а затем обратно в десятичное, все, что превышает 15 цифр, может не дать таких же результатов. Но если у вас есть double и вы хотите преобразовать его в десятичную и обратно, вам нужно 17 цифр. - person James Kanze; 21.10.2014

Я бы предложил что-то вроде

double d = 1.4 / 0.4;
int whole = (int)d;
int nextWhole = whole + 1;

int result = whole;

if (fabs(d - nextWhole) < EPSILON) result = nextWhole;

(это работает для положительных чисел)

По сути, если ваше число настолько близко к следующему целому числу, что это не имеет значения, этот код будет использовать следующее целое число.

person Community    schedule 21.10.2014

Вы можете использовать round()

http://www.cplusplus.com/reference/cmath/round/

Я считаю, что это всегда отнимает 0,5 числа от нуля, поэтому, если это плохо для вашего случая, это может быть не оптимальное решение.

person sedavidw    schedule 21.10.2014