Проблема не в вашем fmod
, а в том, что вы вводите floor
. Для fmod
нормально возвращать что-то близкое к знаменателю из-за нечеткости точности с плавающей точкой. Проблема в том, что вы должны быть осторожны, чтобы относиться к частному с использованием тех же правил, что и к остальным, чтобы получить результаты (используя нечеткое равенство):
x/y == (quot, rem) == quot * y + rem
Для иллюстрации я добавил div4
и div5
:
std::pair<double, double> div4( int num, double denom){
int quo;
auto rem = std::remquo(num, denom, &quo );
return {quo, rem};
}
std::pair<double, double> div5( int num, double denom){
auto whole = std::floor(num / static_cast<long double>( denom ) );
auto remain = std::fmod(num, denom);
return {whole, remain};
}
Вот уменьшенная версия вашего кода , сосредоточенная только на случае сбоя. Выход:
div1: 50 / 16.6666666666666678509 = (whole, remain) = (3, 16.6666666666666642982) = 66.6666666666666571928
...
div4: 50 / 16.6666666666666678509 = (whole, remain) = (3, -3.55271367880050092936e-15) = 50
div5: 50 / 16.6666666666666678509 = (whole, remain) = (2, 16.6666666666666642982) = 50
Для div1
вы получаете целое 3 и остаток (почти) одного делителя. Ошибка в том, что значение, отправляемое в floor
, находится прямо в строке из-за нечеткости с плавающей запятой и поэтому увеличивается до 3, где оно должно быть на самом деле 2.
Если вы используете мой div5
, который использует std::remquo
для вычисления остатка и частного одновременно, вы получите аналогичную пару (2, ~divisor)
, которая затем все умножается правильно обратно на 50. (Обратите внимание, что частное возвращается как целое число, а не число с плавающей запятой из этой стандартной функции.) [ Обновление : как отмечено в комментариях, это действительно только для 3 битов точности в частное, то есть оно полезно для периодических функций, нуждающихся в обнаружении квадранта или октанта, но не общего фактора.]
Или, если вы используете мою div4
, я использовал вашу div1
логику, но перед операцией деления повысил точность ввода с floor
до long double
, что дает достаточно цифр для правильной оценки пола. Результат - (3, ~0)
, который показывает нечеткость в остатке, а не в частном.
Подход long double
, в конечном счете, просто парит банку по дороге к той же проблеме с некоторой более высокой точностью. Использование std::remquo
является более надежным в численном отношении для ограниченных случаев периодических функций. Какую версию вы выберете, будет зависеть от того, что вас больше волнует: числовые вычисления или симпатичный дисплей.
Обновление : Вы также можете попытаться определить, когда что-то пошло не так, используя исключения FP:
void printError()
{
if( std::fetestexcept(FE_DIVBYZERO) ) std::cout << "pole error occurred in an earlier floating-point operation\n";
if( std::fetestexcept(FE_INEXACT) ) std::cout << "inexact result: rounding was necessary to store the result of an earlier floating-point operation\n";
if( std::fetestexcept(FE_INVALID) ) std::cout << "domain error occurred in an earlier floating-point operation\n";
if( std::fetestexcept(FE_OVERFLOW) ) std::cout << "the result of the earlier floating-point operation was too large to be representable\n";
if( std::fetestexcept(FE_UNDERFLOW) ) std::cout << "the result of the earlier floating-point operation was subnormal with a loss of precision\n";
}
// ...
// Calling code
std::feclearexcept(FE_ALL_EXCEPT);
const auto res = div(i, denom);
printError();
// ...
Сообщает inexact result: rounding was necessary to store the result of an earlier floating-point operation
для функций 1, 2, 3 и 5. Смотрите его в прямом эфире на Coliru .