Pull to refresh

Правда ли, что люди пишут безумный код с перекрывающимися побочными эффектами, сохраняя при этом невозмутимость?

Reading time 3 min
Views 37K
Original author: Raymond Chen
Вашему вниманию предлагается перевод статьи Рэймонда Чена из блога The Old New Thing, посвященной проблемам кода, полагающегося на порядок вычисления выражений — и всем тем, кто пишет foo(i++, a[i]);
Порядок вычисления выражений определяется конкретной реализацией, за исключением случаев, когда язык гарантирует определенный порядок вычислений. Если же в дополнение к результату вычисление выражения вызывает изменения в среде выполнения, то говорят, что данное выражение имеет побочные эффекты.
MSDN
В нашей внутренней рассылке про C# регулярно возникает дискуссионный вопрос, который касается корректной интерпретации подобных конструкций:

a -= a *= a;
p[x++] = ++x;

В ответ я спрашиваю:
Да кто вообще пишет такой код с невозмутимым видом? Одно дело, когда такое пишешь, пытаясь победить в «Международном Конкурсе запутывания кода на Си» (IOCCC, International Obfuscated C Code Contest), или если хочешь написать головоломку — но в обоих случаях понимаешь, что ты занимаешься чем-то нестандартным. Что, реально есть кто-то, кто пишет a -= a *= a и p[x++] = ++x; и думает про себя «Чёрт возьми, да я пишу действительно классный код!»
На что Эрик Липперт отвечает мне: «Да, такие люди определенно встречаются». В качестве примера он привел одну успешную книгу популярного автора, который свято верил в то, что чем короче код, тем быстрее он работает. Так вот, представьте себе — продажи этой книги составляют уже свыше 4 миллионов копий и продолжают расти. Автор этой книги постарался впихнуть в каждое выражение несколько побочных эффектов сразу, плотно усеяв их условными тернарными операторами; всё дело в том, что он искренне верил в то, что скорость выполнения программы пропорциональна количеству использованных в ней точек с запятыми — и что каждый раз, когда программист объявляет новую переменную, Бог убивает щеночка.

Разумеется, приложив некоторые усилия, можно научить компилятор анализировать такой код и выдавать предупреждение вроде такого: "Результат этой операции может различаться в зависимости от порядка вычисления". Однако, в случае принятия подобного решения, вам придется иметь дело с другими проблемами.

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

total_cost = p->base_price + p->calculate_tax();

Этот код вызовет предупреждение: компилятор увидит, что метод calculate_tax не является константным (const), поэтому он обеспокоится тем, что метод может изменить переменную base_price — и в этом случае иметь значение будет то, считаете ли вы налог по оригинальной base_price базовой цене, или по уже измененной. Теперь, допустим, что вы знаете (и эти знания компилятору недоступны), что метод подсчета налога calculate_tax обновляет значение локальной переменной налог (tax) в объекте, но не изменяет базовую цену; итак, для вас это предупреждение будет ложной тревогой.

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

Что ж, хорошо, сделаем шаг назад, и давайте будем предупреждать только о самых вопиющих случаях — только тогда, когда переменная изменяется и вычисляется в одном и том же выражении. "Предупреждение: Выражение полагается на порядок вычисления".

Возьмём супер-умного программиста Эксперта Джо: он знает, что его код безупречен, а компилятор — слабак. «Хорошо, да это же очевидно, что сначала делается инкремент переменной, затем она используется для вычисления индекса массива, а затем результат поиска в массиве сохраняется обратно в переменную. Здесь нет конфликта порядка вычисления. Тупой компилятор!». В результате супер-умный программист Эксперт Джо отключит это предупреждение, сочтя его бесполезным. Что ж, наш Эксперт Джо — это всё-таки безнадежный случай, и за него мы не беспокоимся.

Но возьмем другого программиста, Новичка Джо — на деле, он даже не поймёт сути этого предупреждения. «Ну ок, давайте посмотрим. Я компилировал эту функцию пять раз, и каждый раз я получал одинаковый результат. Результат выглядит для меня надежным. Похоже на то, что предупреждение было ложным». Таким образом, как раз те, кто должен был получить пользу от этого предупреждения, не всегда обладает достаточными знаниями, чтобы понять его.

Конечно же, проходит некоторое время, и этот вопрос всплывает в рассылке вновь. Кто-нибудь обязательно спросит, почему выражение x ^= y ^= x ^= y не работает в C#, хотя работает в С++. Вот вам ещё одно доказательство того, что некоторые всё-таки пишут код, который полагается на несколько побочных эффектов сразу — и эти же люди искренне считают, что их код очевиден и гарантировано будет работать.

Ссылка на оригинал
Ссылка на обсуждение
Tags:
Hubs:
+62
Comments 277
Comments Comments 277

Articles