Ноль в степени ноль равен единице!!! (многозначительный трагический взгляд)
Ну, думаю, бывает всякое и разное. Решил проверить функцию pow() отдельно, наивно полагая, что словлю хотя бы исключение:
#include <cmath>
#include <iostream>
#include <stdexcept>
int main(void) {
try {
std::cout << "zeroth power of zero: " << std::pow(0, 0) << std::endl;
}
catch(std::exception &ex) {
std::cerr << ex.what() << std::endl;
}
return 0;
}
Ожидания подтвердились — никаких исключений (все в полном соответствии с документацией), результат в консоли — упорная единица.
Позволю себе небольшое отступление на минутку науки. Если совсем коротко и упрощенно, то:
Возведение в степень — бинарная операция, при которой число a умножается само на себя столько раз, сколько указано в показателе степени b. Записывается это как ab и читается "a в степени b". Например:
35 = 3*3*3*3*3 или 52 = 5*5
Но как быть, если показатель степени равен нулю? Как должна выглядеть запись после знака равенства?.. Решение оказалось довольно простым — в силу свойств степени, число ab можно представить в виде ab = ac * a(b-c). Для наглядности:
47 = (4*4*4)*(4*4*4*4) = 43 * 44
Число же в нулевой степени можно представить как a0 = ab * a-b. Но что это за отрицательная степень такая, как понимать? Ответ довольно прост: действие, обратное умножению — деление. Так, число в отрицательной степени означает единицу, деленную на число в положительной степени — a-b = 1/ab.
Исходя из вышенаписанного, мы можем сделать довольно простой вывод:
a0 = ab * a-b = ab/ab = 1
Но как быть в случае, если a = 0? Возникает довольно неприятная ситуация — деление нуля на ноль, на который делить строго запрещено под страхом возникновения сверхмассивных черных дыр.
Есть и другие способы вычисления нуля в нулевой степени и одни дают единицу, другие ноль, третьи опять деление на ноль. Такая ситуация, когда что-то не поддается вычислению, в математике называется неопределенностью (нельзя определить однозначно).
Ну вот, кратенький ликбез закончен.
Теперь вернемся снова к программированию… Решил я проверить как дела обстоят в других языках и запустил Python:
import math
try:
print("0**0:", 0**0)
print("pow(0, 0)", math.pow(0, 0))
except:
print("Exception")
И этот змей выдает единицу и никаких ошибок. Да что ж такое-то?! Полез в эти ваши интернеты и наткнулся на вот такой список. Вот ведь подсуропили нам ребятки — почти во всех языках выдается единица, вместо ожидаемой ошибки, как, например, при делении на ноль.
И такое встречается в браузерных калькуляторах той же корпорации добра, яндекса, стандартных системных. И это, по моему скромному мнению, чертовски серьезная ошибка. Поэтому, не удивляйтесь, когда увидите в коде:
T correct_pow(T a, T b) {
if(a == 0 && b == 0) {
throw zero_power_zero();
}
}
return(pow(a, b));
Комментарии (29)
4dmonster
10.03.2016 15:48+14В математике результат возведения в нулевую степень не определён, а в программировании:
The IEEE 754-2008 floating point standard is used in the design of most floating point libraries. It recommends a number of functions for computing a power:[39]
pow treats 00 as 1. This is the oldest defined version. If the power is an exact integer the result is the same as for pown, otherwise the result is as for powr (except for some exceptional cases).
pown treats 00 as 1. The power must be an exact integer. The value is defined for negative bases; e.g., pown(?3,5) is ?243.
powr treats 00 as NaN (Not-a-Number – undefined). The value is also NaN for cases like powr(?3,2) where the base is less than zero. The value is defined by epower?log(base).
PapaBubaDiop
10.03.2016 17:41+10В математике результат возведения в нулевую степень не определён
40 лет назад нас учили, что определен.
0serg
10.03.2016 17:32+11Это не ошибка. Это совершенно верное, хотя и для кого-то неожиданное поведение. Есть целый ряд математических причин по которым удобно считать что pow(0,0)=1 и это поведение является частью Стандарта в ряде языков и, как выше уже отметили, в спецификации IEEE 754 на "общую" функцию pow
VaalKIA
10.03.2016 17:33Озвучте, пожалуйста, эти математические причины.
0serg
10.03.2016 17:51+5Удобно считать что pow(x,y) определена специальным образом для случаев когда y является целым числом, потому что этим случаям можно дать специальную интерпретацию. Например исходя из того что (-2)^2=4, удобно доопределить что pow(-2,2)=4 хотя вообще говоря pow(x,y) не определена (точнее является комплекснозначной) для x<0.
Возьмите теперь, к примеру, ряд Фурье для экспоненты:
exp(x) = \sum_{n=0...\inf} x^n / n!
Или возьмите правило дифференцирования d/dx (x^p) = p x^{p-1}
Попробуйте посчитать в этих формулах exp(0) или d/dx(x) и Вы убедитесь, что 0^0 удобно считать равным 1, дабы не обрабатывать эти ситуации как специальные.Moonrise
11.03.2016 19:34А есть куча математических ситуаций, в которых результат другой. Так что оправдание слабоватенькое.
pow(x,y) не определена (точнее является комплекснозначной) для x<0.
Точнее определена только для целых y. Комплексные числа вообще ни при чём.
potan
10.03.2016 17:52+5Обычно разумным решением является считать функцию максимально непрерывной. pow(n*x,x) стремится 1 при x стремится к нулю сверху. Так что выбор удачен.
Moonrise
11.03.2016 19:28+1а pow(0, x) стремится к нулю.
а pow(pow(0.5, 1/x), x) стремится к 0.5
Никакой непрерывностью тут и не пахнет.
kosmos89
10.03.2016 18:15+1If a domain error occurs, an implementation-defined value is returned (NaN where supported)
Вообще, <cmath> не кидает эксепшенов. У вас целые числа, значит вернет, как компилятор решит, на что вы жалуетесь? Что документацию не читаете?
UA3MQJ
10.03.2016 19:57В Erlang тот же результат:
1> math:pow(0,0). 1.0
kahi4
10.03.2016 20:18+2В любом языке программирования под x86 по очевидным причинам одинаковое поведение этой функции. Собственно, потому что x86 реализует его по стандарту IEEE 754 (точнее, стандарт был основан на i8087 в свое время, не столь важно). Исключения — языки, реализующие операции с плавающей точкой самостоятельно (всякие длинные арифметики и прочее).
MichaelBorisov
10.03.2016 21:43-2Это вы еще не сталкивались со случаями неопределенного поведения в C++!
В С++, если вы напишете следующий код:
int8_t i = 127;
i++;
То поведение вашей программы будет неопределенным. Может быть, полетит исключение. А может быть и нет. Что будет в переменной i после выполнения инкремента — неизвестно. Любые инструкции языка, стоящие после i++, могут выполняться неожиданным и причудливым образом. В общем, гуглите "undefined behavior в C++" — вам откроется новая Вселенная.alexeyrom
11.03.2016 12:31+1Любые инструкции языка, стоящие после i++, могут выполняться неожиданным и причудливым образом
Излишне оптимистично. Те, которые стоят раньше, тоже могут, если бы их нормальное выполнение привело бы к переполнению.
MichaelBorisov
10.03.2016 21:54Даже в математике возведение нуля в нулевую степень не определено. Читаем Википедию:
Выражение 0^0 (ноль в нулевой степени) принято считать лишённым смысла[8][9][10], то есть неопределённым.
Связано это с тем, что функция двух переменных x^y в точке \{0,0\} имеет неустранимый разрыв. (В самом деле, вдоль положительного направления оси X, где y=0, она равна единице, а вдоль положительного направления оси Y, где x=0, она равна нулю.)
А в языках C и C++ не принято проверять все подобные случаи. Иначе программа будет тормозить от таких проверок. Вместо этого обычно считается, что в результате запрещенной операции поведение программы оказывается неопределенным: она может делать все, что угодно, возвращать какие угодно числа, сбоить, бросать исключения и даже (стандартом это не запрещено, а некоторыми адептами — поощряется) отформатировать ваш диск. Так что будьте аккуратны.alexeyrom
11.03.2016 12:25+1Это не пример неопределённого поведения в C++. Возвращение 1 здесь абсолютно гарантировано стандартом.
MichaelBorisov
11.03.2016 21:43Да, в самом деле. Удивительно даже. Сколько более безобидных вещей в C/C++ приводит к неопределенному поведению, а этот вопиющий случай попытки вычисления математического выражения, лишенного смысла — нет.
Тем не менее, возвращаемое значение является по стандарту implementation-defined (а не обязательно 1). Также возможна генерация математического исключения, если они разрешены.
Вот из стандарта C99:
The pow functions compute x raised to the power y. A domain error occurs if x is finite and negative and y is finite and not an integer value. A range error may occur. A domain error may occur if x is zero and y is zero. A domain error or range error may occur if x is zero and y is less than zero.
On a domain error, the function returns an implementation-defined value; if the integer expression math_errhandling & MATH_ERRNO is nonzero, the integer expression errno acquires the value EDOM; if the integer expression math_errhandling & MATH_ERREXCEPT is nonzero, the ‘‘invalid’’ floating-point exception is raised.
Так что некоторая неопределенность остается. Ну и вообще я бы не рекомендовал писать программы, поведение которых зависит от обработки компилятором или интерпретатором подобных ситуаций — вычисление неопределенных математических выражений.
Zifix
10.03.2016 23:08+5Я вас только об одном прошу — постарайтесь воздержаться от использования исключений в C++, хотя бы для проверки таких простых вещей. Оно может работать на порядок медленнее и вносить ненужные сложности в отладку. Многие крупные проекты — как Chromium или Qt, написаны вообще без использования исключений, и все работает отлично. Если производительность не критична, то лучше писать на более высокоуровневых языках, где исключения дешевле.
alexeyrom
11.03.2016 12:24+2Вот ведь подсуропили нам ребятки — почти во всех языках выдается единица, вместо ожидаемой ошибки, как, например, при делении на ноль.
Вообще-то при делении на ноль в числах с плавающей точкой никакой ошибки нет: возвращается бесконечность с соответствующим знаком (или NaN, если числитель тоже ноль или NaN).
maaGames
Подозреваю, что ответ прост: if( n == 0 ) return 1;
pow принимает и дробную степень, так что простым перемножением не обойтись. Проверка степени на нуль весьма хорошая оптимизация с одним побочным эффектом, на который вы и наткнулись. А исключений в сишке не было…
impwx
Судя по комменту 4dmonster, проблема в самом стандарте представления дробных чисел, на который все современные компиляторы опираются.
Исключений в C не было, но в том же IEEE-754 предусмотрен NaN, возвращаемый при дробном делении на ноль, вычислении квадратного корня отрицательного числа и прочих арифметических пакостях. Тут он был бы вполне уместен.
0serg
В определенном "математическом" смысле и pow(-2,1) — это тоже NaN. Но это неудобно, согласны? Хочется чтобы отрицательное число все ж таки можно было возвести, скажем, в квадрат используя функцию pow(). И в рамках этого подхода когда pow(x,y) рассматривается не как самостоятельная непрерывная действительнозначная функция, а как часть целого ряда математических формул, удобно определять pow(0,0)=1
impwx
Не понял. Почему
pow(-2,1)
будет равноNaN
и почему это неудобно?0serg
Поэтому существует мнение что есть две разные действительнозначные функции:
и есть "convenience function" pow(x,y) которая для целых y совпадает для pown, а для всех остальных с powr.
При этом pown(0,0)=1. А дальше либо Вы говорите что надо пользоваться "правильным" powr, но тогда случаи x<0 вылетают, либо таки используете "удобное" определение и тогда pow(0,0)=1. Можно еще конечно взять комплекснозначную powc(x,y), но там тоже будут свои проблемы связанные с неоднозначностью её определения.