Доброго времени суток!

Какое Ваше отношение к оператору goto в языках С/С++? Скорее всего, когда Вы учились программировать, Вы его использовали. Потом Вы узнали, что это плохо, и Вы о нем позабыли. Хотя иногда при сложной обработке ошибок… нет-нет, там try … throw … catch. Или же для выхода из вложенных циклов … не-ет, там флаги и куча сложностей. Или когда вложенные switch … нет-нет-нет, там те же флаги.
И все-таки, иногда в ночной тиши Вы допускали в свое подсознание грешную мысль – «а почему бы не использовать вот тут goto? И программа вроде как стройней будет, и оптимально выходит. Да-а, было бы хорошо… Но нет – нельзя, забыли!».
А почему так оно?
Под катом – небольшое расследование и мое, основанное на многолетней практике и разных платформах, отношение к этому вопросу. Эта статья — аналог такой же для С++, но здесь выделены моменты именно для С и для микроконтроллеров.
Просьба к ярым противникам goto – не свергайте меня в геенну огненную минусовой кармы только из-за того, что я поднял эту тему и являюсь частичным сторонником goto!
Тем, кто и без меня прекрасно знает, что такое комбинационная схема, схема с памятью, и как из этого вырос ассемблер – можно смело перескакивать далее – к выводу.

Вначале было слово – и слово это было функция. Не так уж и важно, что это была булева функция от логической переменной – потом в этом базисе умудрились реализовать всю (почти) математику, а потом и тексты, графику… Как бы то ни было, оказалось, что с помощью вычислительной техники очень удобно делать арифметические, а потом тригонометрические и прочие действия и находить значения функций от переменной.
Другими словами, Вам нужно было сделать устройство, которое по значению переменной (переменных) нахо��ило значение функции.
Для решения этой сложнейшей задачи строился последовательный алгоритм для выполнения арифметических операций (в случае заданной точности вычислений в таком алгоритме каждое арифметическое действие можно выполнять за один такт).
Имея алгоритм, несложно построить комбинационную схему – схему, которая мгновенно (с точностью до срабатывания логических устройств и времени распространения сигналов) на выходе давала ответ.
Вопрос – тут нужны какие-нибудь переходы? Нет, их тут просто-напросто нет. Есть последовательное течение действий. Все эти действия можно реализовать в конечном счете за один такт (не спорю, это будет очень и очень громоздко, но задавшись разрядностью всех данных, такую схему Вам построит любой студент – и тем более синтезатор для VHDL или Verilog).

А потом чья-то умная голова додумалась до схемы с обратной связью – например, RS-триггер. И тогда появилось состояние схемы. А состояние – это ни что иное, как текущее значение всех элементов с памятью.
Появление таких элементов памяти позволило сделать революционный скачок вперед от жестко заданных устройств к микропрограммным автоматам. Упрощенно говоря, в микропрограммных автоматах есть память команд. Есть отдельное устройство, которое реализует текущую микропрограмму (сложение, вычитание или еще чего). А вот выбором «текущей» микропрограммы занимается отдельное устройство – пусть это будет «устройство выборки».
Вопрос – тут есть какие-нибудь переходы? Однозначно да! Более того, появляются переходы безусловные (адрес следующей команды не зависит от текущего состояния данных) и условные (адрес следующей команды зависит от состояния данных).
Можно ли без них обойтись? Да никак! Если не использовать переходы, то мы вернемся к комбинационной схеме без памяти.
Апофеозом таких вычислительных устройств стали микро-, просто- и супер-компьютеры. Все они в основе имеют язык кодов, достаточно легко преобразуемый в Ассемблер с приблизительно совпадающим набором команд. Рассмотрим некий усредненный ассемблер микроконтроллеров (я знаком с ассемблером для ATmeg-и, PIC и AT90). Как у него построена работа переходов?
Переходы бывают безусловные (просто переход на следующий адрес, переход в подпрограмму, выход из нее) и условные (в зависимости от состояния флагов).
Со всей ответственностью заявляю – без операций перехода в ассемблере обойтись невозможно! Любая программа на ассемблере просто таки пестрит ими! Впрочем, тут со мной никто спорить, я думаю, не будет.
Какой итог можно подвести? На уровне микропроцессора операции перехода используются очень активно. Реальную программу, их не использующую, написать почти невозможно (может быть, ее можно сделать, но это будет супер-мега-извращение и точно уж не реальная программа!). С этим тоже спорить никто не будет.
Но почему же тогда в языках более высокого уровня – сконцентрируемся на С для микроконтроллеров — опер��тор goto вдруг впал в немилость?..

А теперь посмотрим на хитровывернутый алгоритм. Представление не имею что это за бред – но его надо реализовать. Будем считать, это такое ТЗ.
Здесь A, B, C, D, E — это некоторые операции, а не вызов функции! Вполне возможно, что они используют массу локальных переменных. И вполне возможно, что они меняют их состояние. Т. е. в данном случае речь не идет о вызове функций — некоторые действия, не будем детализировать.
Вот как это выглядит с goto:
Очень лаконично и читабельно. Но — нельзя! Попробуем без goto:
Вы что-нибудь поняли из логики работы второго листинга?..
Сравним оба листинга:
Но зато во втором листинге нет goto!
Еще мне предлагали этот алгоритм реализовать приблизительно так:
Вроде бы красиво, да? Почему бы не сделать copy-past, почему бы не накрутить дополнительные проверки, почему бы… сделать все что угодно, кроме goto!!!
Ну да ладно, в жизни такие алгоритмы почти не встречаются. Лучше поговорим о жизни.
Я за свой более чем 20-летний стаж прошел несколько аппаратных платформ и с десяток языков программирования, участвовал в написании крупного программного продукта ActiveHDL, делал коммерческую базу данных и много небольших программ для отладки оборудования, используемого в Олимпийских играх, а также делал устройства для этой самой Олимпиады (уже несколько Олимпиад, если быть точным). Короче, что-то я в программировании шарю. А, да, забыл – я закончил с почетным дипломом ХНУРЭ — то бишь, в теории я тоже секу.
Поэтому мои последующие размышления и ситуации… скажем так, я имею моральное право на них.
В языке С есть много операторов, которые на самом деле являются банальным goto – условным или безусловным. Это все виды циклов for (…), while (…) {…}, do {…} while (…). Это анализ числовых переменных switch (…) {case … case …}. Это те же операторы прерывания/перехода в циклах break и continue. В конце концов, Это вызовы функций funct() и выход из них return.
Эти goto считаются «легальными» — чем же нелегален сам goto?
Обвиняют его в том, что код становится нечитабельным, плохо оптимизируемым и могут появиться ошибки. Это про практические минусы. А теоретические – это просто плохо и неграмотно, и все тут!
Насчет нечитабельности кода и плохой оптимизируемости – еще раз взгляните на листинги выше.
Насчет вероятности появления ошибок – согласен, такой код воспринимается несколько сложнее из-за того, что мы привыкли читать листинг сверху вниз. Но и все! А что, другие средства С безопасные и не могут создать ошибок в коде? Да взять хотя бы преобразования типов, работа с указателями. Там напортачить — за нечего делать!
Вам не кажется, что нож – это очччень опасная вещь? Но почему-то на кухне мы им пользуемся. А 220 вольт – ужас как опасно! Но если пользоваться с умом – жить можно.
Тоже самое и goto. Пользоваться им надо с умом – и тогда код будет работать корректно.
А про теоретические доводы – это, уж простите меня, спор о вкусах. Вы пользуетесь Венгерской нотацией? Я – нет, терпеть ее не могу! Но я ж не говорю, что она плохая из-за этого! Лично я считаю, что переменная должна нести смысловую нагрузку – для чего она создана. Но я не буду запрещать пользоваться этим способом именования другим людям!
Или же есть эстеты, которые считают, что писать a = ++i неграмотно, надо писать i = i + 1; a = i. И что теперь, запретить и это тоже?
Возьмем обработку входных пакетов с некоего внешнего устройства:
Мы получили заголовок пакета. Проанализировали. Ага, пакет 'A' — значит, нам надо 10 раз сделать чего-то. Мы не забываем контролировать время работы этого участка — а вдруг вторая сторона зависла? Ага, таки зависла — сработало условие timeout — тогда выходим наружу — из цикла, из switch.
Тоже самое можно сделать и с помощью всяческих флагов — но работать это будет медленней, плюс ко всему займет такую дефицитную ячейку памяти. Оно того стоит?
Это случай простой. А ведь receive_byte () тоже может быть макрофункцией с обработкой таймаутов. И там тоже будут такие вот резкие выходы.
Это как раз тот самый случай, где я активно использую goto. Это мне позволило не попадать в «зависания» в случае проблем с внешними устройствами, UART, USB и т. п.
Посмотрите на программу ниже:
Что происходит – понятно? Есть вложенный цикл. Если наступило какое-то условие – покидаем все последующие обработки.
Данный код с флагами выглядит иначе:
Что произошло в данном случае? На каждой итерации мы теперь проверяем флаг. Не забываем его проверять и дальше. Это мелочи, если итераций немного и речь идет о «безразмерной» памяти у PC. А когда программа написана для микроконтроллера – это все уже становится существенно.
Кстати, в связи с этим в некоторых языках (если не ошибаюсь, в Java) есть возможность выйти из цикла по метке вида break Leave. Тот же goto, между прочим!
Точно такой же пример я могу привести и с обработкой в switch (…) { case …}. С этим я сталкиваюсь часто при обработке входящих пакетов неодинаковой структуры.
Знакомы ли Вы с автоматным программированием? Или любым другим автоматизированным созданием кода? Скажем, создатели лексических обработчиков (без использования громоздкого boost::spirit). Все эти программы создают код, который можно использовать как «черный ящик» — Вам не важно, что там внутри; Вам важно, что он делает. А внутри там goto используется очень и очень часто…
На С иногда приходится писать что-то вроде:
Этот код гораздо аккуратней будет выглядеть так:
Идея понятна? Иногда надо при выходе что-то сделать. Иногда много чего надо сделать. И тогда тут здорово помогает goto. Такие примеры у меня тоже имеются.
Вроде бы все перечислил, теперь можно подвести…
Это моя точка зрения! И она справедлива для меня. Может – и для Вас, но я не буду Вас заставлять ей следовать!
Так вот, для меня очевидно, что goto помогает оптимальней и качественней решить некоторые проблемы.
А бывает и наоборот – goto может породить массу проблем.
UPD: Начитавшись гору комментариев, я для себя выделил положительные стороны использования goto и отрицательные.
Плюсы использования goto:
Минусы использования goto:
Кто еще подскажет плюсы/минусы? Впишу, если они будут оправданы.
Еще раз обращаю внимание: я не призываю тулить goto везде! НО в некоторых случаях он позволяет реализовать алгоритм куда эффективней всех остальных средств.

Какое Ваше отношение к оператору goto в языках С/С++? Скорее всего, когда Вы учились программировать, Вы его использовали. Потом Вы узнали, что это плохо, и Вы о нем позабыли. Хотя иногда при сложной обработке ошибок… нет-нет, там try … throw … catch. Или же для выхода из вложенных циклов … не-ет, там флаги и куча сложностей. Или когда вложенные switch … нет-нет-нет, там те же флаги.
И все-таки, иногда в ночной тиши Вы допускали в свое подсознание грешную мысль – «а почему бы не использовать вот тут goto? И программа вроде как стройней будет, и оптимально выходит. Да-а, было бы хорошо… Но нет – нельзя, забыли!».
А почему так оно?
Под катом – небольшое расследование и мое, основанное на многолетней практике и разных платформах, отношение к этому вопросу. Эта статья — аналог такой же для С++, но здесь выделены моменты именно для С и для микроконтроллеров.
Просьба к ярым противникам goto – не свергайте меня в геенну огненную минусовой кармы только из-за того, что я поднял эту тему и являюсь частичным сторонником goto!
Небольшой исторический экскурс
Тем, кто и без меня прекрасно знает, что такое комбинационная схема, схема с памятью, и как из этого вырос ассемблер – можно смело перескакивать далее – к выводу.
А все начиналось с комбинационных схем

Вначале было слово – и слово это было функция. Не так уж и важно, что это была булева функция от логической переменной – потом в этом базисе умудрились реализовать всю (почти) математику, а потом и тексты, графику… Как бы то ни было, оказалось, что с помощью вычислительной техники очень удобно делать арифметические, а потом тригонометрические и прочие действия и находить значения функций от переменной.
Другими словами, Вам нужно было сделать устройство, которое по значению переменной (переменных) нахо��ило значение функции.
Для решения этой сложнейшей задачи строился последовательный алгоритм для выполнения арифметических операций (в случае заданной точности вычислений в таком алгоритме каждое арифметическое действие можно выполнять за один такт).
Имея алгоритм, несложно построить комбинационную схему – схему, которая мгновенно (с точностью до срабатывания логических устройств и времени распространения сигналов) на выходе давала ответ.
Вопрос – тут нужны какие-нибудь переходы? Нет, их тут просто-напросто нет. Есть последовательное течение действий. Все эти действия можно реализовать в конечном счете за один такт (не спорю, это будет очень и очень громоздко, но задавшись разрядностью всех данных, такую схему Вам построит любой студент – и тем более синтезатор для VHDL или Verilog).
Но потом вмешались схемы с памятью

А потом чья-то умная голова додумалась до схемы с обратной связью – например, RS-триггер. И тогда появилось состояние схемы. А состояние – это ни что иное, как текущее значение всех элементов с памятью.
Появление таких элементов памяти позволило сделать революционный скачок вперед от жестко заданных устройств к микропрограммным автоматам. Упрощенно говоря, в микропрограммных автоматах есть память команд. Есть отдельное устройство, которое реализует текущую микропрограмму (сложение, вычитание или еще чего). А вот выбором «текущей» микропрограммы занимается отдельное устройство – пусть это будет «устройство выборки».
Вопрос – тут есть какие-нибудь переходы? Однозначно да! Более того, появляются переходы безусловные (адрес следующей команды не зависит от текущего состояния данных) и условные (адрес следующей команды зависит от состояния данных).
Можно ли без них обойтись? Да никак! Если не использовать переходы, то мы вернемся к комбинационной схеме без памяти.
В итоге мы пришли к ассемблеру
Апофеозом таких вычислительных устройств стали микро-, просто- и супер-компьютеры. Все они в основе имеют язык кодов, достаточно легко преобразуемый в Ассемблер с приблизительно совпадающим набором команд. Рассмотрим некий усредненный ассемблер микроконтроллеров (я знаком с ассемблером для ATmeg-и, PIC и AT90). Как у него построена работа переходов?
Переходы бывают безусловные (просто переход на следующий адрес, переход в подпрограмму, выход из нее) и условные (в зависимости от состояния флагов).
Со всей ответственностью заявляю – без операций перехода в ассемблере обойтись невозможно! Любая программа на ассемблере просто таки пестрит ими! Впрочем, тут со мной никто спорить, я думаю, не будет.
Итог
Какой итог можно подвести? На уровне микропроцессора операции перехода используются очень активно. Реальную программу, их не использующую, написать почти невозможно (может быть, ее можно сделать, но это будет супер-мега-извращение и точно уж не реальная программа!). С этим тоже спорить никто не будет.
Но почему же тогда в языках более высокого уровня – сконцентрируемся на С для микроконтроллеров — опер��тор goto вдруг впал в немилость?..
Немного об алгоритмах

А теперь посмотрим на хитровывернутый алгоритм. Представление не имею что это за бред – но его надо реализовать. Будем считать, это такое ТЗ.
Здесь A, B, C, D, E — это некоторые операции, а не вызов функции! Вполне возможно, что они используют массу локальных переменных. И вполне возможно, что они меняют их состояние. Т. е. в данном случае речь не идет о вызове функций — некоторые действия, не будем детализировать.
Вот как это выглядит с goto:
if (a)
{
A;
goto L3;
}
L1:
if (b)
{
L2:
B;
L3:
C;
goto L1;
}
else if (!c)
{
D;
goto L2;
}
E;
Очень лаконично и читабельно. Но — нельзя! Попробуем без goto:
char bf1, bf2, bf3;
if (a)
{
A;
bf1 = 1;
}
else
bf1 = 0;
bf2 = 0;
do
{
do
{
if (bf3 || b)
bf3 = 1;
else
bf3 = 0;
if (bf3 || bf2)
B;
if (bf3 || bf1 || bf2)
{
C;
bf1 = 0;
bf2 = 1;
}
if (!bf3)
{
if (!c)
{
D;
bf3 = 1;
}
else
{
bf3 = 0;
bf2 = 0;
}
}
}
while (bf3);
}
while (bf2);
E;
Вы что-нибудь поняли из логики работы второго листинга?..
Сравним оба листинга:
- На первый листинг я потратил раз в 5 меньше времени, чем на второй.
- Листинг с goto короче как минимум в 2 раза.
- Листинг с goto поймет любой человек с самой минимальной подготовкой в С. Второй же я постарался сделать максимально доступным и очевидным – и все равно, в него надо долго вникать.
- Сколько времени уйдет на отладку первого варианта и сколько на отладку второго?
- И вообще, если считать нарис��ванный алгоритм постановкой задачи, то первый листинг правильный на 100%. Про второй я до сих пор не очень уверен… хотя бы в очередности проверки условий и флагов.
- Сравните получившийся ассемблерный код первого и второго листинга.
Но зато во втором листинге нет goto!
Еще мне предлагали этот алгоритм реализовать приблизительно так:
if a
A
C
while b or not c
if not b
D
B
C
E
Вроде бы красиво, да? Почему бы не сделать copy-past, почему бы не накрутить дополнительные проверки, почему бы… сделать все что угодно, кроме goto!!!
Ну да ладно, в жизни такие алгоритмы почти не встречаются. Лучше поговорим о жизни.
goto в реальных программах
Я за свой более чем 20-летний стаж прошел несколько аппаратных платформ и с десяток языков программирования, участвовал в написании крупного программного продукта ActiveHDL, делал коммерческую базу данных и много небольших программ для отладки оборудования, используемого в Олимпийских играх, а также делал устройства для этой самой Олимпиады (уже несколько Олимпиад, если быть точным). Короче, что-то я в программировании шарю. А, да, забыл – я закончил с почетным дипломом ХНУРЭ — то бишь, в теории я тоже секу.
Поэтому мои последующие размышления и ситуации… скажем так, я имею моральное право на них.
Неявное использование goto
В языке С есть много операторов, которые на самом деле являются банальным goto – условным или безусловным. Это все виды циклов for (…), while (…) {…}, do {…} while (…). Это анализ числовых переменных switch (…) {case … case …}. Это те же операторы прерывания/перехода в циклах break и continue. В конце концов, Это вызовы функций funct() и выход из них return.
Эти goto считаются «легальными» — чем же нелегален сам goto?
В чем обвиняют goto
Обвиняют его в том, что код становится нечитабельным, плохо оптимизируемым и могут появиться ошибки. Это про практические минусы. А теоретические – это просто плохо и неграмотно, и все тут!
Насчет нечитабельности кода и плохой оптимизируемости – еще раз взгляните на листинги выше.
Насчет вероятности появления ошибок – согласен, такой код воспринимается несколько сложнее из-за того, что мы привыкли читать листинг сверху вниз. Но и все! А что, другие средства С безопасные и не могут создать ошибок в коде? Да взять хотя бы преобразования типов, работа с указателями. Там напортачить — за нечего делать!
Вам не кажется, что нож – это очччень опасная вещь? Но почему-то на кухне мы им пользуемся. А 220 вольт – ужас как опасно! Но если пользоваться с умом – жить можно.
Тоже самое и goto. Пользоваться им надо с умом – и тогда код будет работать корректно.
А про теоретические доводы – это, уж простите меня, спор о вкусах. Вы пользуетесь Венгерской нотацией? Я – нет, терпеть ее не могу! Но я ж не говорю, что она плохая из-за этого! Лично я считаю, что переменная должна нести смысловую нагрузку – для чего она создана. Но я не буду запрещать пользоваться этим способом именования другим людям!
Или же есть эстеты, которые считают, что писать a = ++i неграмотно, надо писать i = i + 1; a = i. И что теперь, запретить и это тоже?
Обработка ошибок
Возьмем обработку входных пакетов с некоего внешнего устройства:
pack = receive_byte ();
switch (pack)
{
case ‘A’:
for (f = 0; f < 10; ++f)
{
…
if (timeout)
goto Leave;
…
}
break;
case ‘B’:
…
}
Leave:
…
Мы получили заголовок пакета. Проанализировали. Ага, пакет 'A' — значит, нам надо 10 раз сделать чего-то. Мы не забываем контролировать время работы этого участка — а вдруг вторая сторона зависла? Ага, таки зависла — сработало условие timeout — тогда выходим наружу — из цикла, из switch.
Тоже самое можно сделать и с помощью всяческих флагов — но работать это будет медленней, плюс ко всему займет такую дефицитную ячейку памяти. Оно того стоит?
Это случай простой. А ведь receive_byte () тоже может быть макрофункцией с обработкой таймаутов. И там тоже будут такие вот резкие выходы.
Это как раз тот самый случай, где я активно использую goto. Это мне позволило не попадать в «зависания» в случае проблем с внешними устройствами, UART, USB и т. п.
Выход из вложенного цикла наружу
Посмотрите на программу ниже:
char a, b, c;
for (a = 0; a < 10; ++a)
{
for (b = 0; b < a; ++b)
{
if (!c)
goto Leave;
}
for (b = 10; b < 15; ++b)
{
d ();
}
}
Leave:
e ();
Что происходит – понятно? Есть вложенный цикл. Если наступило какое-то условие – покидаем все последующие обработки.
Данный код с флагами выглядит иначе:
char a, b, c, f1;
f1 = 1;
for (a = 0; a < 10 && f1; ++a)
{
for (b = 0; b < a && f1; ++b)
{
if (!c)
f1 = 0;
}
if (f1)
{
for (b = 10; b < 15; ++b)
{
d ();
}
}
}
e ();
Что произошло в данном случае? На каждой итерации мы теперь проверяем флаг. Не забываем его проверять и дальше. Это мелочи, если итераций немного и речь идет о «безразмерной» памяти у PC. А когда программа написана для микроконтроллера – это все уже становится существенно.
Кстати, в связи с этим в некоторых языках (если не ошибаюсь, в Java) есть возможность выйти из цикла по метке вида break Leave. Тот же goto, между прочим!
Точно такой же пример я могу привести и с обработкой в switch (…) { case …}. С этим я сталкиваюсь часто при обработке входящих пакетов неодинаковой структуры.
Автоматическое создание кода
Знакомы ли Вы с автоматным программированием? Или любым другим автоматизированным созданием кода? Скажем, создатели лексических обработчиков (без использования громоздкого boost::spirit). Все эти программы создают код, который можно использовать как «черный ящик» — Вам не важно, что там внутри; Вам важно, что он делает. А внутри там goto используется очень и очень часто…
Выход в одном месте
На С иногда приходится писать что-то вроде:
int f (…)
{
…
if (a)
{
c = 15;
return 10;
}
…
if (b)
{
c = 15;
return 10;
}
…
с = 10;
return 5;
}
Этот код гораздо аккуратней будет выглядеть так:
int f (…)
{
…
if (a)
goto Exit;
…
if (b)
goto Exit;
…
с = 10;
return 5;
Exit:
c = 15;
return 10;
}
Идея понятна? Иногда надо при выходе что-то сделать. Иногда много чего надо сделать. И тогда тут здорово помогает goto. Такие примеры у меня тоже имеются.
Вроде бы все перечислил, теперь можно подвести…
Итог
Это моя точка зрения! И она справедлива для меня. Может – и для Вас, но я не буду Вас заставлять ей следовать!
Так вот, для меня очевидно, что goto помогает оптимальней и качественней решить некоторые проблемы.
А бывает и наоборот – goto может породить массу проблем.
UPD: Начитавшись гору комментариев, я для себя выделил положительные стороны использования goto и отрицательные.
Плюсы использования goto:
- самый оптимальный (с т. зр. листинга и результирующего кода) выход из нескольких вложенных циклов и switch… case
- Си: наиболее экономичный (по листингу и результирующему коду) способ обработки ошибок
- в отдельно взятых случаях самое оптимальное построение алгоритма
- экономит память и такты при аккуратном использовании, что иной раз бывает первостепенно важным
Минусы использования goto:
- непривычность кода
- нарушение хода следования чтения листинга сверху вниз и стандартизированного обхода блоков в коде (в смысле, что возможен переход в центр блока, а также выход из его)
- усложнение компилятору (а иногда и невозможность) процесса оптимизации кода
- повышение вероятности создания трудноуловимых ошибок в коде
Кто еще подскажет плюсы/минусы? Впишу, если они будут оправданы.
Еще раз обращаю внимание: я не призываю тулить goto везде! НО в некоторых случаях он позволяет реализовать алгоритм куда эффективней всех остальных средств.