Почему Goto опасно?
goto
не вызывает нестабильности сам по себе. Несмотря на примерно 100 000 goto
с, ядро Linux остается моделью стабильности.
goto
само по себе не должно вызывать уязвимостей в безопасности. Однако в некоторых языках смешивание его с блоками управления try
/ catch
исключений может привести к уязвимости, как объяснено в этой рекомендации CERT . Основные компиляторы C ++ помечают и предотвращают такие ошибки, но, к сожалению, старые или более экзотические компиляторы этого не делают.
goto
вызывает нечитаемый и не поддерживаемый код. Это также называется кодом спагетти , потому что, как и в тарелке спагетти, очень трудно следить за потоком контроля, когда слишком много gotos.
Даже если вам удастся избежать спагетти-кода и если вы используете только несколько gotos, они все равно способствуют таким ошибкам, как утечка ресурсов:
- Код, использующий структурное программирование с четкими вложенными блоками и циклами или переключателями, прост для понимания; поток управления очень предсказуем. Поэтому проще обеспечить соблюдение инвариантов.
- С помощью
goto
заявления вы нарушаете этот простой поток и разбиваете ожидания. Например, вы можете не заметить, что вам еще нужно освободить ресурсы.
- Многие
goto
в разных местах могут отправить вас к одной цели goto. Так что не очевидно, чтобы точно знать, в каком состоянии вы находитесь, достигнув этого места. Следовательно, риск ошибочных / необоснованных предположений достаточно велик.
Дополнительная информация и цитаты:
C предоставляет бесконечно злоупотребляемую goto
инструкцию и метки для перехода к. Формально goto
это никогда не требуется, и на практике почти всегда легко написать код без него. (...)
Тем не менее, мы предложим несколько ситуаций, где goto может найти место. Наиболее распространенное использование - отказаться от обработки в некоторых глубоко вложенных структурах, таких как разрыв двух циклов одновременно. (...)
Хотя мы не догматичны в этом вопросе, похоже, что утверждения goto следует использовать с осторожностью, если вообще .
Когда можно пойти?
Как и K & R, я не догматичен по поводу gotos. Я признаю, что бывают ситуации, когда goto может облегчить жизнь.
Как правило, в C goto разрешает многоуровневый выход из цикла или обработку ошибок, требующую достижения соответствующей точки выхода, которая освобождает / разблокирует все ресурсы, которые были выделены до сих пор (т.е. многократное распределение в последовательности означает несколько меток). Эта статья дает количественную оценку различных вариантов использования goto в ядре Linux.
Лично я предпочитаю избегать этого, и через 10 лет я использовал максимум 10 gotos. Я предпочитаю использовать вложенные if
s, которые я считаю более читабельными. Когда это приведет к слишком глубокому вложению, я решу либо разложить свою функцию на более мелкие части, либо использовать логический индикатор в каскаде. Современные оптимизирующие компиляторы достаточно умны, чтобы генерировать практически тот же код, что и тот же код, с которым goto
.
Использование goto сильно зависит от языка:
В C ++ правильное использование RAII заставляет компилятор автоматически уничтожать объекты, которые выходят за пределы области видимости, так что ресурсы / блокировки все равно будут очищены, и больше нет необходимости в goto.
В Java нет необходимости Гото (см автора цитаты в Java выше , и этот превосходный Stack Overflow ответ ): сборщик мусора , который очищает беспорядок, break
, continue
и try
/ catch
обработка исключений охватывает все тот случай , когда goto
может быть полезными, но в более безопасном и лучше манера. Популярность Java доказывает, что на современном языке можно избежать утверждения goto.
Увеличьте известную уязвимость SSL goto fail
Важный отказ от ответственности: ввиду жесткой дискуссии в комментариях я хочу пояснить, что я не претендую на то, что утверждение goto является единственной причиной этой ошибки. Я не делаю вид, что без goto не было бы ошибки. Я просто хочу показать, что goto может быть связано с серьезной ошибкой.
Я не знаю, сколько серьезных ошибок связано с goto
историей программирования: детали часто не сообщаются. Однако была известная ошибка Apple SSL, которая ослабила безопасность iOS. Утверждение, которое привело к этой ошибке, было неверным goto
.
Некоторые утверждают, что основной причиной ошибки был не сам оператор goto, а неправильное копирование / вставка, вводящее в заблуждение отступ, отсутствие фигурных скобок вокруг условного блока или, возможно, рабочие привычки разработчика. Я не могу ни подтвердить ни один из них: все эти аргументы являются вероятными гипотезами и интерпретациями. Никто на самом деле не знает. ( между тем, гипотеза о слиянии, которая пошла не так, как кто-то предложил в комментариях, кажется очень хорошим кандидатом ввиду некоторых других несоответствий отступов в той же функции ).
Единственный объективный факт заключается в том, что дублирование goto
привело к преждевременному выходу из функции. Если посмотреть на код, единственным другим оператором, который мог бы вызвать такой же эффект, был бы возврат.
Ошибка в функции SSLEncodeSignedServerKeyExchange()
в этом файле :
if ((err = ReadyHash(&SSLHashSHA1, &hashCtx)) != 0)
goto fail;
if ((err =...) !=0)
goto fail;
if ((err = SSLHashSHA1.update(&hashCtx, &signedParams)) != 0)
goto fail;
goto fail; // <====OUCH: INDENTATION MISLEADS: THIS IS UNCONDITIONDAL!!
if (...)
goto fail;
... // Do some cryptographic operations here
fail:
... // Free resources to process error
Действительно, фигурные скобки вокруг условного блока могли бы предотвратить ошибку:
это привело бы либо к синтаксической ошибке при компиляции (и, следовательно, к исправлению), либо к избыточному безопасному переходу. Кстати, GCC 6 сможет обнаружить эти ошибки благодаря своему необязательному предупреждению для обнаружения несовместимых отступов.
Но, во-первых, всех этих проблем можно было бы избежать с помощью более структурированного кода. Таким образом, goto является косвенной причиной этой ошибки. Есть по крайней мере два разных способа, которые могли бы избежать этого:
Подход 1: если оговорка или вложенная if
s
Вместо того, чтобы последовательно проверять множество условий на наличие ошибок и каждый раз отправлять fail
метку в случае возникновения проблемы, можно было бы if
выбрать выполнение криптографических операций в -состоянии, которое делало бы это, только если не было неправильного предварительного условия:
if ((err = ReadyHash(&SSLHashSHA1, &hashCtx)) == 0 &&
(err = ...) == 0 ) &&
(err = ReadyHash(&SSLHashSHA1, &hashCtx)) == 0) &&
...
(err = ...) == 0 ) )
{
... // Do some cryptographic operations here
}
... // Free resources
Подход 2: использовать аккумулятор ошибок
Этот подход основан на том факте, что почти все приведенные здесь операторы вызывают некоторую функцию для установки err
кода ошибки и выполняют остальную часть кода только в том случае, если err
было 0 (то есть функция выполнялась без ошибок). Хорошая безопасная и читаемая альтернатива:
bool ok = true;
ok = ok && (err = ReadyHash(&SSLHashSHA1, &hashCtx))) == 0;
ok = ok && (err = NextFunction(...)) == 0;
...
ok = ok && (err = ...) == 0;
... // Free resources
Здесь нет ни одного перехода: нет риска быстро перейти к точке выхода из строя. И визуально было бы легко обнаружить смещенную линию или забытую ok &&
.
Эта конструкция более компактна. Он основан на том факте, что в C вторая часть логического и ( &&
) оценивается, только если первая часть истинна. Фактически, ассемблер, сгенерированный оптимизирующим компилятором, почти эквивалентен исходному коду с gotos: оптимизатор очень хорошо обнаруживает цепочку условий и генерирует код, который при первом ненулевом возвращаемом значении переходит к концу ( онлайн-подтверждение ).
Вы могли бы даже предусмотреть проверку согласованности в конце функции, которая могла бы на этапе тестирования выявлять несоответствия между флагом ok и кодом ошибки.
assert( (ok==false && err!=0) || (ok==true && err==0) );
Ошибки, такие как ==0
непреднамеренная замена !=0
логическим или ошибочным соединителем, легко обнаружатся на этапе отладки.
Как сказано: я не претендую на то, что альтернативные конструкции позволили бы избежать любой ошибки. Я просто хочу сказать, что они могли затруднить возникновение ошибки.