101

Во многих языках программирования (C, C++, C#, Java, различные диалекты Паскаля, PHP, JavaScript) есть оператор вычисления остатка. Он действует очевидным, единственно верным образом для положительных значений аргумента (17 % 5 == 2), но для отрицательного делимого и положительного делителя он даёт отрицательный результат:

-17 % 5 == -2

Обычное применение оператора %, однако — для вычисления хэшей, индексов в кольцевом буфере, а также вычисление канонического представителя группы чисел, то есть, для представления отношения эквивалентности. Например, номер дня недели естественным образом вычисляется как остаток от деления «глобального» номера дня на 7. Проверка числа на нечётность также определяется остатком при делении на 2.

Однако, оператор % в том виде, как он реализован в упомянутых языках, непригоден без дополнительной обработки: нужна функция наподобие

int mod(int n, int d)
{
    int result = n % d;
    if (sign(result) * sign(d) < 0)
        result += d;
    return result;
}

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

У такой функции, в отличие от %, есть полезный инвариант:

mod(n + k * d, d) == mod(n, d)

(при условии, что вычисление обеих частей не приводят к переполнению).

Приведу несколько примеров.

  1. Проверка на нечётность обычно выглядит так:

    //bool odd = n % 2 == 1; // неправильно! bool odd = n % 2 != 0; // довольно искусственно

Но с новым оператором она работает проще:

bool odd = mod(n, 2) == 1; // как и ожидается.
  1. Для вычисления bucket'а в хэш-таблице тоже применяется остаток:

    int bucketIdx = object.GetHashCode() % buckets.Count; if (bucketIdx < 0) bucketIdx += buckets.Count;

или так (код из .NET 4.0)

int positiveHashCode = object.GetHashCode() & 7FFFFFFF;
int bucketIdx = positiveHashCode % buckets.Count;

В то же время

int bucketIdx = mod(object.GetHashCode(), buckets.Count);

сработал бы в любом случае.

  1. Приведение угла в градусах к каноническому виду (от 0 до 360):

    mod(angle, 360)

В радианах: mod(angle, 2*PI)

То же самое с % выглядит гораздо более тяжеловесно.

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

Есть ли у кого-то примеры (из практики или теоретические), когда нужно именно существующее поведение оператора %?


Двое отвечающих справедливо замечают, что частное q и остаток r при делении n на d должны удовлетворять условию

n == q * d + r

Для того, чтобы это работало, можно переопределить и деление так, чтобы округление выполнялось всегда вниз: q == floor((double)n / (double)d). При этом нужное соотношение будет автоматически выполняться:

 4 / 3 ==  1   4 % 3 == 1      4 =  1 * 3 + 1
 3 / 3 ==  1   3 % 3 == 0      3 =  1 * 3 + 0
 2 / 3 ==  0   2 % 3 == 2      2 =  0 * 3 + 2
 1 / 3 ==  0   1 % 3 == 1      1 =  0 * 3 + 1
 0 / 3 ==  0   0 % 3 == 0      0 =  0 * 3 + 0
-1 / 3 == -1  -1 % 3 == 2     -1 = -1 * 3 + 2
-2 / 3 == -1  -2 % 3 == 1     -2 = -1 * 3 + 1

и т. д.

VladD
  • 206,799
  • 1
    про дробные прогнал, но : http://www.wolframalpha.com/input/?i=17+mod+%287%29 http://www.wolframalpha.com/input/?i=-17+mod+%287%29 – zb' Jun 17 '13 at 12:33
  • 1
    Забавный результат получается, когда делится нацело, а знак разный.

    --

    Если серьезно, то меня больше устраивает, когда результат совпадает с выработанным схемами процессора. Логичное для задачи поведение наверное лучше явно программировать.

    – avp Jun 17 '13 at 13:14
  • 1
    @avp: в таком случае теряется переносимость, разве нет? – VladD Jun 17 '13 at 16:11
  • 1
    @eicto: Wolfram Alpha даёт как раз тот результат, который я бы хотел. – VladD Jun 17 '13 at 16:12
  • 1
    @VladD, да, переносимость теряется, точно также, как она теряется при изменении порядка байт в слове, размера указателя и т.п.

    Машино- (да и системно-) зависимые части переносимой программы просто надо выделять в отдельные файлы и использовать условную трансляцию (впрочем, это сильно зависит от языка).

    – avp Jun 17 '13 at 17:31
  • 1
    @avp: то, что вы говорите, очень в духе C, но не в духе C++. (C++ движется в сторону boost'а, где все системно- и компиляторозависимые штуки спрятаны за «интерфейсом», который перенимает на себя все #ifdef'ы.) – VladD Jun 17 '13 at 17:41
  • 1
    @VladD, конечно, Си.

    Просто в этом я солидарен с Линусом - C++ is a horrible language.

    – avp Jun 17 '13 at 19:41
  • 1
    @avp: опасаюсь даже представить, что думает Линус о моём любимом C#. :-) Насчёт критики C++ гораздо более детально здесь. – VladD Jun 17 '13 at 21:38
  • 1
    @VladD, FQA известная, но весьма длинная и занудная критика С++. Интересно, кто-нибудь ее до конца дочитал?

    --

    По поводу же C# (мнение Линуса мне неизвестно) - IMHO это просто другой (и похоже хороший) язык, сравнивать его с С++ не стоит. Мне кажется, что основная проблема в нем та же, что и в Java - слишком много классов.

    (дискутировать о том, хорошо это или плохо, здесь не удастся - лимит комментариев)

    --

    А вот опасения RMS по поводу шарпа и free software.

    – avp Jun 17 '13 at 22:10
  • 1
    @avp: я дочитал FQA :-) и со многим даже согласен. А насчёт RMS — мне кажется, что Microsoft с тех пор изменились, т.к. потеряли монополию. Вот старшие товарищи подсказывают, что N лет назад самым злым корпоративным монстром была IBM, а сейчас ничего, Линукс поддерживают. В любом случае, это я спорю не с вами, а заочно с RMS. – VladD Jun 17 '13 at 22:29
  • 1
  • Мой алгоритм для словаря: считать весь файл в одну строку, перегнать в юникод, расчитать индексы автоматом. Имеем: две строки (одна удаляется) и список пар целых чисел; один вызов MBtoWC; один проход по каждому символу. Когда нужно отобразить/передать, извлекать подстроку. Когда нужно сравнить, сравнивать на месте без выделения памяти. К слову, так же Regex в .NET работает: в Match хранятся только индексы.
  • – Kyubey Feb 27 '15 at 11:13
  • 1
    Расскажите про ненужность знаний низкоуровневых трюков разработчикам компиляторов, мультимедийных кодеков, игровых движков, демосценерам в конце концов. :) Мы говорим о разной сложности. Не надо мешать в одну кучу сложность системы, алгоритмы, оптимизацию и т.п. Разная сложность — разные инструменты.

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

    – Kyubey Feb 27 '15 at 17:01