10.08.00
Так уж сложилось, что именно присутствие или отсутствие этого
оператора в яызке программирования всегда вызывает
жаркие дебаты среди сторонников "хорошего стиля" программирования.
При этом, и те, кто "за", и те, кто "против" всегда
считают признаком "хорошего тона" именно использование
goto или, наоборот, его неиспользование.
Я не собираюсь изображать из себя сторонника одной
из этих "школ", я просто хочу показать, что действительно
есть места, где использование goto
выглядит вполне логично.
Но сначала о грустном. Обычно в вину goto
ставится то, что его присутствие в языке программирования
позволяет делать примерно такие вещи:
int i, j;
for(i = 0; i < 10; i++)
{
// ...
if(condition1)
{
j = 4;
goto label1;
}
// ...
for(j = 0; j < 10; j++)
{
// ...
label1:
// ...
if(condition2)
{
i = 6;
goto label2;
}
}
// ...
label2:
// ...
}
Прямо скажем, что такое использование goto
несолько раздражает, потому что понять при этом
как работает программа при ее чтении будет очень сложно.
А для человека, который не является ее автором, так
и вообще невозможно. Понятно, что вполне вероятны случаи,
когда такого подхода требует какая-нибудь очень серьезная
оптимизация работы программы, но делать что-то подобное
программист в здравом уме не должен. На самом деле,
раз уж я привел подобный пример, в нем есть еще
один замечательный нюанс ---- изменение значения переменной
цикла внутри цикла. Смею вас заверить, что такое
поведение вполне допустимо внутри do или
while; но когда используется for
такого надо избегать, потому что отличительная черта
for как раз и есть жестко определенное местоположение
инициализации, проверки условия и инкремента (т.е., изменения переменной
цикла). Я к чему: читатель исходного текста, увидев "полный" for
(т.е. такой, в котором заполнены все эти три места) может и не заметить
изменения переменной где-то внутри цикла. Хотя для циклов с небольшим телом это,
наверное, все-таки допустимо --- такая практика обычно применяется
при обработке строк (когда надо, например, считать какой-то символ, который
идет за "спецсимволом", как "\\" в строках на C; вместо того, что бы вводить
дополнительный флаг, значительно проще увидев "\" сразу же сдвинуться на одну
позицию и посмотреть, что находится там). В общем, всегда надо
руководствоваться здравым смыслом и читабельностью программы. Если здравый
смысл по каким-то причинам становится в противовес читабельности программы,
то это место надо обнести красными флагами, чтобы читатель сразу видел
подстерегающие его опасности.
Тем не менее, вернемся к goto. Несмотря на то,
что такое расположние операторов безусловного перехода несколько
нелогично (все-таки, вход внутрь тела цикла это, конечно же,
неправильно), это встречается. Я сам видел три вложенных бесконечных цикла,
расположенных на пятистах строчках исходного текста, внутри которых
примерно такой "зюзей" были расставлены goto. Понять такой
текст, по-моему, просто невозможно.
Итак, противники использования goto в конечном итоге
приходят к подобным примерам и говорят о том, что раз такое его
использование возможно, то лучше что бы его совсем не было. При
этом, конечно же, никто обычно не спорит против применения, например,
break, потому что его действие жестко ограничено.
Хочется сказать, что подобную ситуацию тоже можно довести до абсурда,
потому что я видел программу, в котрой был введен цикл только для
того, что бы внутри его тела использовать break для
выхода из него (т.е., цикл делал только одну итерацию, просто в зависимости
от исходного состояния заканчивался в разных местах). Вот что помешало
автору использовать goto (раз уж хотелось), кроме
догматических соображений, я не понимаю.
Собственно, я как раз подошел к тому, что обычно называется "разумным" применением этого оператора. Вот пример:
switch(key1)
{
case q1 :
switch(key2)
{
case q2 : break;
}
break;
}
Все упрощено до безобразия, но, в принципе, намек понятен. Есть ситуации,
когда нужно что-то в духе break, но на несколько окружающих
циклов или операторов switch, а break завершает
только один. Понятно, что в этом примере читабельность, наверное, не нарушена (в смысле,
использовался бы вместо внутреннего break goto или нет),
единственное, что в таком случае будет выполненно два оператора перехода вместо одного
(break это, все-таки, разновидность goto).
Значительно более показателен другой пример:
bool end_needed = false;
for( ... )
{
for( ... )
{
if(cond1) { end_needed = true; break; }
}
if(end_needed) break;
}
Т.е., вместо того, что бы использовать goto и выйти
из обоих циклов сразу, пришлось завести еще одну переменную и еще одну
проверку условия. Тут хочется сказать, что goto в такой ситуации
выглядит много лучше --- сразу видно, что происходит; а то в этом случае придется
пройти по всем условиями и посмотреть, куда они выведут. Надо сказать (раз
уж я начал приводить примеры из жизни), что я не раз видел эту
ситуацию доведенную до крайности --- четыре вложенных цикла (ну что поделать) и
позарез надо инициировать выход из самого внутреннего. И что? Три лишних проверки...
Кроме того, введение еще одной переменной, конечно же, дает возможность еще
раз где-нибудь допустить ошибку, например, в ее инициализации. Опять же,
читателю исходного текста придется постоянно лазить по тексту и смотреть,
зачем была нужна эта переменная... в общем: не плодите сущностей без надобности. Это
только запутает.
Другой пример разумного использования goto следующий:
int foo()
{
int res;
// ...
if(...)
{
res = 10;
goto finish;
}
// ...
finish:
return res;
}
Понятно, что без goto это выглядело бы как return 10
внутри if. Итак, в чем преимущества такого подхода. Ну, сразу же
надо вспомнить про концептуальность --- у функции становится только один "выход",
вместо нескольких (быстро вспоминаем про IDEF). Правда, концептуальность это вещь
такая... неиспользование goto тоже в своем роде концептуальность, так
что это не показатель (нельзя противопоставлять догму догме, это просто глупо).
Тем не менее, выгоды у такого подхода есть. Во-первых, вполне вероятно, что перед
возвратом из функции придется сделать какие-то телодвижения (закрыть открытый
файл, например). При этом, вполне вероятно, что когда эта функция писалась, этого и не
требовалось --- просто потом пришлось дополнить. И что? Если операторов return
много, то перед каждым из них появится одинаковый кусочек кода. Как это делается?
Правильно, методом "cut&paste". А если потом придется поменять? Тоже верно, "search&replace".
Объяснять почему это неудобно не буду --- это надо принять как данность.
Во-вторых, обработка ошибок, которая так же требует немедленного выхода с возвратом используемых ресурсов. В принципе, в C++ для этого есть механизм исключительных ситуаций, но когда он отсутствует (просто выключен для повышения производительности), это будет работать не хуже. А может и лучше по причине более высокой скорости.
В третьих, упрощается процесс отладки. Всегда можно проверить что возвращает
функция, поставить точку останова (хотя сейчас некоторые отладчики дают
возможность найти все return и поставить на них точки останова),
выставить дополнительный assert или еще что-нибудь в этом духе.
В общем, удобно.
Еще goto очень успешно применятся при автоматическом
создании кода --- читателя исходного текста там не будет, он будет
изучать то, по чему исходный текст был создан, поэтому можно (и нужно)
допускать различные вольности. Для примера посылаю к результатам
lex или yacc.
В принципе, больше навскидку вспомнить примеров оправданного применения
оператора goto я не смог, но это и не надо. Мне просто
хотелось показать, что при правильном использовании он очень полезен.
Надо только соблюдать здравый смысл, но это общая рекомендация
к программированию на С/С++ (да и вообще, на любом языке программирования),
поэтому непонятно почему goto надо исключать.
Оператор goto имеет полное право на существование,
потому что существуют ситуации, в которых он может значительно
улучшить читабельность исходных текстов. Использовать его, или
нет --- решать каждому программисту самостоятельно, но иметь
такую возможность полезно.
| Бъерн Страуструп | Язык программирования C++, 3 издание. |