Russian Qt Forum

Qt => Вопросы новичков => Тема начата: zzzseregazzz от Сентябрь 23, 2013, 16:28



Название: Как корректно обработать деление на 0, логарифм нуля, и т.п.
Отправлено: zzzseregazzz от Сентябрь 23, 2013, 16:28
Заметил, что в таких ситуациях (при работе с типом double) не генерируется никаких исключений.
Когда деления часто встречаются в программе, контроль каждого деления сильно загромождает код. 
QString::number применительно к результату таких операций выдает непонятные надписи вроде "inf".
Как правильно обработать такие ошибки?
Есть ли настройки компиляторов (в Windows и Linux) для того, чтобы в таких ситуациях бросались исключения?
Стоит ли написать и везде использовать класс-обёртку к базовым типам?




Название: Re: Как корректно обработать деление на 0, логарифм нуля, и т.п.
Отправлено: Igors от Сентябрь 23, 2013, 16:39
Это называется nan (not a number). Имеет свойство быстро размножаться т.к. любая опреация с ним производит только nan. Решение - проверять все аргументы, напр на ноль не делить.  Желание сачкануть (генреация исключений) в данном случае неуместно, т.к. проверка всяких граничных условий есть часть работы.


Название: Re: Как корректно обработать деление на 0, логарифм нуля, и т.п.
Отправлено: zzzseregazzz от Сентябрь 23, 2013, 16:50
Если деление в каждой 2-й строке, то что тогда делать?
Проверить аргументы недостаточно, т.к. 0 может получиться в процессе вычислений.


Название: Re: Как корректно обработать деление на 0, логарифм нуля, и т.п.
Отправлено: Igors от Сентябрь 23, 2013, 17:11
Если деление в каждой 2-й строке, то что тогда делать?
То что все делают - проверять и подсекать значения до нужного/валидного диапазона.

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

Если вылазит nan, значит входные данные некорректны - значит надо или автоматом делать их корректными или давать пользователю отлуп с указанием что не так. А выскакивать исключением здесь неграмотно


Название: Re: Как корректно обработать деление на 0, логарифм нуля, и т.п.
Отправлено: Авварон от Сентябрь 23, 2013, 20:04
А ведь на форуме выкладывали библиотечку, преобразующую SIG* в исключения:)


Название: Re: Как корректно обработать деление на 0, логарифм нуля, и т.п.
Отправлено: zzzseregazzz от Сентябрь 24, 2013, 08:08
То что все делают - проверять и подсекать значения до нужного/валидного диапазона.

Например как результат деления на 0 вернуть 1e+100 ? А почему не 3e+200? Нельзя некоректные данные сделать корректными.


Название: Re: Как корректно обработать деление на 0, логарифм нуля, и т.п.
Отправлено: zzzseregazzz от Сентябрь 24, 2013, 08:11
Значит надо сохранить напр образовавшийся делитель во временной переменной и ее провеоить.

Если проверять каждое деление может существенно упасть производительность.
Не говоря уже о читаемости кода.


Название: Re: Как корректно обработать деление на 0, логарифм нуля, и т.п.
Отправлено: Igors от Сентябрь 24, 2013, 09:55
Если проверять каждое деление может существенно упасть производительность.
Не говоря уже о читаемости кода.
Я занимаюсь в основном инженерными расчетами и аккуратно проверяю всю арифметику. Либы которые я испльзую делают так же, насколько я могу судить по исходникам. Практически любой расчет имеет крайние/вырожденные случаи которые надо корректно отработать. Ничего особо страшного нет, проверки достаточно редки, бросаться на каждое деление не надо.

Напр в исходном массиве треугольников могут быть такие что их площадь слишком мала или нулевая. Да, придется их сначала отфильтровать и удалить из контейнера, иначе быстро получу nan'ы. А выбросив исключение - что дальше? С этими данными не работаем, а почему - хз. Часто только этим профессиональный софт отличается от любительского.


Название: Re: Как корректно обработать деление на 0, логарифм нуля, и т.п.
Отправлено: zzzseregazzz от Сентябрь 24, 2013, 11:41
Простой пример. Надо отобразить отношение сигнал/шум в логарифмическом масштабе.
 
Что делать, если
- нет ни сигнала ни шума - log(0/0)
- сигнал есть, шум нулевой - log(1/0)
- сигнала нет, шум есть - log(0)

В первых двух случаях получаем сразу 2 неопределённости, но при этом все ситуации вполне корректны.

Как представить результат в виде строки?



Название: Re: Как корректно обработать деление на 0, логарифм нуля, и т.п.
Отправлено: Igors от Сентябрь 24, 2013, 12:04
Не знаю причем здесь строка. Ввести порог шума и сигнала - достаточно малые числа. типа 1.0e-8, это должно быть известно по задаче. Ну или просто if'ами. В любом случае exception здесь неуместны, т.к. ситуация штатная, корректная (по крайней мере с точки зрения пользователя), и обработке подлежит


Название: Re: Как корректно обработать деление на 0, логарифм нуля, и т.п.
Отправлено: zzzseregazzz от Сентябрь 24, 2013, 12:25
Т.е. из 0 делаем 1.0e-8. При этом считаем в несколько этапов - вместо
Код
C++ (Qt)
double sn=log(s/n);
пишем
Код
C++ (Qt)
double n_=((n<=0)?1e-8:n);
double sn_=s/n_;
double sn__=((sn_<=0)?1e-8:sn_);
double sn=log(sn__);
Боюсь, что это не пройдет code review из-за большого объёма и плохой читаемости, даже без учета логики (а если написать через if, то тем более).

Пусть пришел сигнал 1e-8, а шум нулевой.
Вместо 0 подставляем 1e-8. т.е. получается, что сигнал равен шуму - а это совсем неправильно.


Название: Re: Как корректно обработать деление на 0, логарифм нуля, и т.п.
Отправлено: m_ax от Сентябрь 24, 2013, 13:28
Да прибавьте в числителе и знаменателе (к шуму и сигналу) малую величину epsilon (cut-off)

Код
C++ (Qt)
static const double epsilon = 0.000001;
 
double sn = log((s + epsilon)/(n + epsilon));
 


Название: Re: Как корректно обработать деление на 0, логарифм нуля, и т.п.
Отправлено: Igors от Сентябрь 24, 2013, 13:33
Код
C++ (Qt)
double sn=log(s/n);
пишем
Код
C++ (Qt)
double n_=((n<=0)?1e-8:n);
double sn_=s/n_;
double sn__=((sn_<=0)?1e-8:sn_);
double sn=log(sn__);
Боюсь, что это не пройдет code review из-за большого объёма и плохой читаемости,
Точно не пройдет, но никто не мешает записать это нормально, напр
Код
C++ (Qt)
double signal = GetSignal();
double noise = GetNoise();
if (Max(signal. noise) < fudge_Factor)
result = 0;
else {
double ratio = Clamp(signal, signal_Min, signal_Max) / Clamp(noise. noise_Min, noise_Max);
result = log(ratio);
}


Название: Re: Как корректно обработать деление на 0, логарифм нуля, и т.п.
Отправлено: zzzseregazzz от Сентябрь 24, 2013, 16:50
Ну вот добрались уже до 8 строчек вместо одной.
К тому же следует учесть, что все функции теоретически могут бросить исключение, и должны быть обернуты в try{}catch{}.


Название: Re: Как корректно обработать деление на 0, логарифм нуля, и т.п.
Отправлено: Igors от Сентябрь 24, 2013, 18:05
Ну вот добрались уже до 8 строчек вместо одной.
Не надо стремиться написать так
Код:
double sn_=s/n_;
Так пишут женщины - и то не все  :)

К тому же следует учесть, что все функции теоретически могут бросить исключение, и должны быть обернуты в try{}catch{}.
Ни разу не видел чтобы напр sqrt, log. acos и.т.п. вызывали исключение. Дал некорректный аргумент - получил nan, вот и вся любовь. А если мы хотим идти своим путем, то проще бросить так
Код
C++ (Qt)
if (sn != sn) throw;


Название: Re: Как корректно обработать деление на 0, логарифм нуля, и т.п.
Отправлено: m_ax от Сентябрь 24, 2013, 19:02
Цитировать
Точно не пройдет, но никто не мешает записать это нормально, напр
Ну это, конечно, нормально.. да)  

Банальная сдвижка сигнала и шума на маааленькую величину решает все эти проблемы)
А чтоб не осталось сомнений в том, что эта маааленькая величина не вносит хоть какие-либо искажения в конечный результат привожу два графика этого логарифма при различных epsilon (0.001 и 0.0001). Даже при таких больших значениях epsilon, я не вижу разницы между графиками..

Код
C++ (Qt)
int main()
{
   std::ofstream out("test.txt");
 
   std::uniform_real_distribution<double> dist(0.0, 1.0);
   std::mt19937 gen;
 
   const double epsilon = 0.001;
   const unsigned N = 100;
 
   for (unsigned i = 0; i < N; ++i) {
       double s = dist(gen);
       double n = dist(gen);
       out << i << " " << log((s + epsilon)/(n + epsilon)) << std::endl;
   }
 
   return 0;
}
 

  


Название: Re: Как корректно обработать деление на 0, логарифм нуля, и т.п.
Отправлено: zzzseregazzz от Сентябрь 25, 2013, 08:07
Цитировать
Банальная сдвижка сигнала и шума на маааленькую величину решает все эти проблемы)
 
Код
C++ (Qt)
double sn = log((s + epsilon)/(n + epsilon));

А вдруг s или n оказались отрицательными? Они могли быть получены с помощью какой-то внешней функции, и кто знает какие там косяки.

Цитировать
Ни разу не видел чтобы напр sqrt, log. acos и.т.п. вызывали исключение.
Кто их знает? Обычно не бросают, но может быть есть такие ОС, в которых бросают.


Название: Re: Как корректно обработать деление на 0, логарифм нуля, и т.п.
Отправлено: Igors от Сентябрь 25, 2013, 10:27
Кто их знает? Обычно не бросают, но может быть есть такие ОС, в которых бросают.
Кто = стандарт, от ОС не зависит.

А вдруг s или n оказались отрицательными? Они могли быть получены с помощью какой-то внешней функции, и кто знает какие там косяки.
Совершенно верно, поэтому я привел пример где Clamp загоняет их в валидный диапазон. Это часть работы, пусть не самая творческая, но которую надо делать, а не сачковать


Название: Re: Как корректно обработать деление на 0, логарифм нуля, и т.п.
Отправлено: m_ax от Сентябрь 25, 2013, 10:45
А вдруг s или n оказались отрицательными? Они могли быть получены с помощью какой-то внешней функции, и кто знает какие там косяки.

Код
C++ (Qt)
double sn = log((fabs(s) + epsilon)/(fabs(n) + epsilon));
 

Ну а если уж так хочется считать логарифм и от отрицательного аргумента, то комплексные числа никто не отменял (см. std::complex)

 


Название: Re: Как корректно обработать деление на 0, логарифм нуля, и т.п.
Отправлено: xokc от Сентябрь 26, 2013, 08:56
А вдруг s или n оказались отрицательными? Они могли быть получены с помощью какой-то внешней функции, и кто знает какие там косяки.
А Вы сами что предполагаете рисовать на графике, если один из "косячных" случаев всё-же случился?

Кто их знает? Обычно не бросают, но может быть есть такие ОС, в которых бросают.
Ну с такой паранойей и i++ оборачивать try/catch нужно.


Название: Re: Как корректно обработать деление на 0, логарифм нуля, и т.п.
Отправлено: zzzseregazzz от Сентябрь 26, 2013, 10:39
Цитировать
Ну с такой паранойей и i++ оборачивать try/catch нужно.

А как же. Надо проверить, не будет ли переполнения.
Т.е. что-то вроде
Код
C++ (Qt)
try {
   if(i<0xffffffff) i++;
   else throw "Integer overflow";
}
catch(const char* ex)
    //...
}
Но это не кроссплатформенно, int не всегда 32 бита. Как это можно исправить?


Название: Re: Как корректно обработать деление на 0, логарифм нуля, и т.п.
Отправлено: zzzseregazzz от Сентябрь 26, 2013, 10:49
А Вы сами что предполагаете рисовать на графике, если один из "косячных" случаев всё-же случился?

Ну, бесконечность можно изобразить вертикальной прямой.
Отрицательные числа, когда они недопустимы, заменить на 0.
Что делать с 0/0 вообще не знаю.
Ваши варианты?


Название: Re: Как корректно обработать деление на 0, логарифм нуля, и т.п.
Отправлено: Igors от Сентябрь 26, 2013, 12:50
А как же. Надо проверить, не будет ли переполнения.
Т.е. что-то вроде
Код
C++ (Qt)
try {
   if(i<0xffffffff) i++;
   else throw "Integer overflow";
}
catch(const char* ex)
    //...
}
Но это не кроссплатформенно, int не всегда 32 бита. Как это можно исправить?
Код
C++ (Qt)
std::numeric_limits<int>::max()
 
Хотя мне неизвестна ни одна современная платформа где int не 4 байта. Вот long да, где 4 а где и 8.

Вы так упорно стремитесь к исключениям как будто это хорошо/правильно и даже круто :) В действительности же ничего хорошего в этом нет. Ну выбросили исключение, дальше-то что? Сообщить пользователю "ошибка" - это не сделает его счастливым, тем более что ситуация вполне штатная

Что делать с 0/0 вообще не знаю.
Ваши варианты?
Да как угодно, напр не рисовать вообще (только должна быть надпись "нет сигнала") или рисовать др цветом или пунктирной линией. Это вопрос дызайна.


Название: Re: Как корректно обработать деление на 0, логарифм нуля, и т.п.
Отправлено: zzzseregazzz от Сентябрь 26, 2013, 13:19
Сообщить пользователю "ошибка" - это не сделает его счастливым, тем более что ситуация вполне штатная

Конечно не сделает.
Но корректно обработать ошибку еще сложнее. А ошибка может произойти почти в каждой строке, даже i++. Получается, надо на каждую строку писать обработчик. В этом обработчике в свою очередь тоже могут возникнуть ошибки...
И какой в итоге будет объём кода, где все возможные ошибки корректно обрабатываются?


Название: Re: Как корректно обработать деление на 0, логарифм нуля, и т.п.
Отправлено: mutineer от Сентябрь 26, 2013, 14:22
Сообщить пользователю "ошибка" - это не сделает его счастливым, тем более что ситуация вполне штатная

Конечно не сделает.
Но корректно обработать ошибку еще сложнее. А ошибка может произойти почти в каждой строке, даже i++. Получается, надо на каждую строку писать обработчик. В этом обработчике в свою очередь тоже могут возникнуть ошибки...
И какой в итоге будет объём кода, где все возможные ошибки корректно обрабатываются?

Если учитывать все возможные ошибки, даже в i++, то объём такого кода будет 0 строк. Потому что в условиях, когда нет уверенности даже в элементарных операциях, писать что-либо невозможно


Название: Re: Как корректно обработать деление на 0, логарифм нуля, и т.п.
Отправлено: xokc от Сентябрь 26, 2013, 14:30
Ну, бесконечность можно изобразить вертикальной прямой.
Бесконечно вертикальной?

Отрицательные числа, когда они недопустимы, заменить на 0.
Так всё-таки допустимы отрицательные числа или нет? Если - нет, то делайте фильтр на входе типа s = qMax(1e-10, s) и n = qMax(1e-10, n) и тему можно закрывать. Если да - то Вы сначала сами для себя решите, чему равен log(-1), а потом мы Вам подскажем как это нарисовать.
Вообще говоря, в обработке сигналов обычно имеет смысл говорить об отношении мощности сигнала к мощности шума. Эти величины заведомо неотрицательны, что сразу снимает все Ваши переживания.

Что делать с 0/0 вообще не знаю.
Ваши варианты?
С учетом того, что решается задача всего лишь отображения графика, мой вариант (он неоднократно уже озвучен в этом топике) - прибавлять к сигналу и шуму по пренебрежимо малой величине. Вариант неоднократно опробирован при решении самых разных задач ЦОС, требующих гораздо большей точности, чем рисование графиков.