Решил я намедни сделать для себя небольшой, но очень удобный велосипед для вычисления всяких полезных математических функций. Стоит отметить, что пишу я на разных языках, и в этот раз выбор пал на C++. Пилю я, значит, сей чудесный трехколесный транспорт и параллельно занимаюсь юнит-тестированием свежесозданных функций… И тут нате-здрасте — один из тестов выдает мне совсем не тот результат, которого я ждал. Готовы?!

Ноль в степени ноль равен единице!!! (многозначительный трагический взгляд)

Ну, думаю, бывает всякое и разное. Решил проверить функцию 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)


  1. maaGames
    10.03.2016 15:41
    +2

    Подозреваю, что ответ прост: if( n == 0 ) return 1;
    pow принимает и дробную степень, так что простым перемножением не обойтись. Проверка степени на нуль весьма хорошая оптимизация с одним побочным эффектом, на который вы и наткнулись. А исключений в сишке не было…


    1. impwx
      10.03.2016 15:59

      Судя по комменту 4dmonster, проблема в самом стандарте представления дробных чисел, на который все современные компиляторы опираются.
      Исключений в C не было, но в том же IEEE-754 предусмотрен NaN, возвращаемый при дробном делении на ноль, вычислении квадратного корня отрицательного числа и прочих арифметических пакостях. Тут он был бы вполне уместен.


      1. 0serg
        10.03.2016 17:40
        +1

        В определенном "математическом" смысле и pow(-2,1) — это тоже NaN. Но это неудобно, согласны? Хочется чтобы отрицательное число все ж таки можно было возвести, скажем, в квадрат используя функцию pow(). И в рамках этого подхода когда pow(x,y) рассматривается не как самостоятельная непрерывная действительнозначная функция, а как часть целого ряда математических формул, удобно определять pow(0,0)=1


        1. impwx
          10.03.2016 17:53
          -2

          Не понял. Почему pow(-2,1) будет равно NaN и почему это неудобно?


          1. 0serg
            10.03.2016 18:10
            +2

            1. log(-2) не определен среди вещественных чисел :)
            2. Функция pow(x,y) для общих x,y определяется по непрерывности через предельный переход, но при x<0 подобный переход в действительных числах провести невозможно

            Поэтому существует мнение что есть две разные действительнозначные функции:

            1. функция pown(x,y) определенная только для целых y и любых x
            2. функция powr(x,y) определенная для любых y, но только неотрицательных x
              и есть "convenience function" pow(x,y) которая для целых y совпадает для pown, а для всех остальных с powr.

            При этом pown(0,0)=1. А дальше либо Вы говорите что надо пользоваться "правильным" powr, но тогда случаи x<0 вылетают, либо таки используете "удобное" определение и тогда pow(0,0)=1. Можно еще конечно взять комплекснозначную powc(x,y), но там тоже будут свои проблемы связанные с неоднозначностью её определения.


  1. 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).


    1. NTP
      10.03.2016 16:56

      Именно в этом и проблема — это не очевидно. Если бы я не тестировал, то это стало бы багом (по крайней мере, в моей программе). Поэтому и предупредил тех, кому такая особенность реализации неизвестна.


      1. alexeyrom
        11.03.2016 12:19

        Это особенность не реализации, а стандарта.


    1. PapaBubaDiop
      10.03.2016 17:41
      +10

      В математике результат возведения в нулевую степень не определён

      40 лет назад нас учили, что определен.


  1. 0serg
    10.03.2016 17:32
    +11

    Это не ошибка. Это совершенно верное, хотя и для кого-то неожиданное поведение. Есть целый ряд математических причин по которым удобно считать что pow(0,0)=1 и это поведение является частью Стандарта в ряде языков и, как выше уже отметили, в спецификации IEEE 754 на "общую" функцию pow


    1. VaalKIA
      10.03.2016 17:33

      Озвучте, пожалуйста, эти математические причины.


      1. 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, дабы не обрабатывать эти ситуации как специальные.


        1. Moonrise
          11.03.2016 19:34

          А есть куча математических ситуаций, в которых результат другой. Так что оправдание слабоватенькое.

          pow(x,y) не определена (точнее является комплекснозначной) для x<0.

          Точнее определена только для целых y. Комплексные числа вообще ни при чём.



  1. potan
    10.03.2016 17:52
    +5

    Обычно разумным решением является считать функцию максимально непрерывной. pow(n*x,x) стремится 1 при x стремится к нулю сверху. Так что выбор удачен.


    1. Moonrise
      11.03.2016 19:28
      +1

      а pow(0, x) стремится к нулю.
      а pow(pow(0.5, 1/x), x) стремится к 0.5
      Никакой непрерывностью тут и не пахнет.


  1. kosmos89
    10.03.2016 18:15
    +1

    If a domain error occurs, an implementation-defined value is returned (NaN where supported)
    Вообще, <cmath> не кидает эксепшенов. У вас целые числа, значит вернет, как компилятор решит, на что вы жалуетесь? Что документацию не читаете?


  1. UA3MQJ
    10.03.2016 19:57

    В Erlang тот же результат:

    1> math:pow(0,0).
    1.0


    1. kahi4
      10.03.2016 20:18
      +2

      В любом языке программирования под x86 по очевидным причинам одинаковое поведение этой функции. Собственно, потому что x86 реализует его по стандарту IEEE 754 (точнее, стандарт был основан на i8087 в свое время, не столь важно). Исключения — языки, реализующие операции с плавающей точкой самостоятельно (всякие длинные арифметики и прочее).


  1. xiWera
    10.03.2016 20:19

    Почему-то в памяти возникает школьные знания из которых следует, что возведение любого числа в нулевую степень равно единице. В том числе и нуля. Но это так давно было, могу и ошибаться.


    1. EndUser
      10.03.2016 21:27

      http://xkcd.ru/263/


    1. fzn7
      11.03.2016 12:23
      +1

      За исключением нуля


  1. MichaelBorisov
    10.03.2016 21:43
    -2

    Это вы еще не сталкивались со случаями неопределенного поведения в C++!

    В С++, если вы напишете следующий код:

    int8_t i = 127;
    i++;

    То поведение вашей программы будет неопределенным. Может быть, полетит исключение. А может быть и нет. Что будет в переменной i после выполнения инкремента — неизвестно. Любые инструкции языка, стоящие после i++, могут выполняться неожиданным и причудливым образом. В общем, гуглите "undefined behavior в C++" — вам откроется новая Вселенная.


    1. alexeyrom
      11.03.2016 12:31
      +1

      Любые инструкции языка, стоящие после i++, могут выполняться неожиданным и причудливым образом
      Излишне оптимистично. Те, которые стоят раньше, тоже могут, если бы их нормальное выполнение привело бы к переполнению.


  1. MichaelBorisov
    10.03.2016 21:54

    Даже в математике возведение нуля в нулевую степень не определено. Читаем Википедию:

    Выражение 0^0 (ноль в нулевой степени) принято считать лишённым смысла[8][9][10], то есть неопределённым.
    Связано это с тем, что функция двух переменных x^y в точке \{0,0\} имеет неустранимый разрыв. (В самом деле, вдоль положительного направления оси X, где y=0, она равна единице, а вдоль положительного направления оси Y, где x=0, она равна нулю.)

    А в языках C и C++ не принято проверять все подобные случаи. Иначе программа будет тормозить от таких проверок. Вместо этого обычно считается, что в результате запрещенной операции поведение программы оказывается неопределенным: она может делать все, что угодно, возвращать какие угодно числа, сбоить, бросать исключения и даже (стандартом это не запрещено, а некоторыми адептами — поощряется) отформатировать ваш диск. Так что будьте аккуратны.


    1. alexeyrom
      11.03.2016 12:25
      +1

      Это не пример неопределённого поведения в C++. Возвращение 1 здесь абсолютно гарантировано стандартом.


      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.

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


  1. Zifix
    10.03.2016 23:08
    +5

    Я вас только об одном прошу — постарайтесь воздержаться от использования исключений в C++, хотя бы для проверки таких простых вещей. Оно может работать на порядок медленнее и вносить ненужные сложности в отладку. Многие крупные проекты — как Chromium или Qt, написаны вообще без использования исключений, и все работает отлично. Если производительность не критична, то лучше писать на более высокоуровневых языках, где исключения дешевле.


  1. alexeyrom
    11.03.2016 12:24
    +2

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

    Вообще-то при делении на ноль в числах с плавающей точкой никакой ошибки нет: возвращается бесконечность с соответствующим знаком (или NaN, если числитель тоже ноль или NaN).