Является ли GOTO безобидным при переходе на очистку в конце функции?

Оператор goto был подробно рассмотрен в нескольких обсуждениях SO (см. this и что), и я, конечно, не хочу оживлять эти горячие дебаты.

Вместо этого я хотел бы сосредоточиться на одном случае использования goto и обсудить его значение и возможные альтернативы.

Рассмотрим следующий фрагмент кода, который является общим для (по крайней мере, моего) FSM:

while (state = next_state()) {
        switch (state) {
                case foo:
                        /* handle foo, and finally: */
                        if (error) goto cleanup;
                        break;
                case bar:
                        /* handle bar, and finally: */
                        if (error) goto cleanup;
                        break;
                /* ...other cases... */
        }
}

return ok;

cleanup:
/* do some cleanup, i.e. free() local heap requests, adjust global state, and then: */
return error;

Переключение элемента очистки в отдельную функцию просто для сохранения goto кажется неудобным. С другой стороны, мы были подняты, чтобы осудить использование goto, где это возможно.

Мой вопрос: мой пример кода считается хорошим стилем?

Если нет, существуют ли доступные альтернативы?

Пожалуйста, придерживайтесь конкретного использования goto, описанного выше. Я не хочу вникать в еще одно обсуждение об общем использовании goto.

Ответы

Ответ 1

Ваше использование goto в порядке. Это не нарушает 2 хороших способа использования goto.

  • goto ДОЛЖЕН спуститься (несколько строк) в источнике
  • Самый внутренний блок goto labels ДОЛЖЕН содержать инструкции goto

Ответ 2

Вместо того, чтобы извлекать логику очистки в свою собственную функцию и вызывать ее из разных мест, я хотел бы рассмотреть вопрос об извлечении оператора switch в отдельную функцию и возвращении от него кода ошибки. В цикле while вы можете проверить код возврата и выполнить очистку и вернуть, если это необходимо.

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

Ответ 3

Goto не требуется, если у вас есть switch. Использование switch и goto просто добавляет сложности.

while (state) {
        switch (state) {
                case cleanup:
                        /* do some cleanup, i.e. free() local heap requests, adjust global state, and then: */
                        return error;
                case foo:
                        /* handle foo, and finally: */
                        if (error) { state = cleanup; continue; }
                        break;
                case bar:
                        /* handle bar, and finally: */
                        if (error) { state = cleanup; continue; }
                        break;
                /* ...other cases... */
        }
        state = next_state();
}

return ok;

Ответ 4

Я бы сказал, если код очистки не может быть обобщен, т.е. он специфичен для функции, в которой он используется, goto - это хороший и чистый способ сделать это.

Ответ 5

Я видел, что goto используется таким образом в ядре OpenBSD, особенно в драйверах устройств ATA (один такой пример), и я лично считаю, что это хороший стиль, поскольку он помогает проиллюстрировать, что именно происходит и как код соответствует соответствующему FSM. При попытке проверить функциональность FSM на спецификацию это использование goto несколько улучшает четкость.

Ответ 6

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

Я не против goto при инициализации нескольких вещей вместе и с простым кодом обработки ошибок, как обсуждалось здесь. Но если вы столкнулись с проблемой создания конечного автомата, то я не вижу достаточной причины для их использования. ИМО, вопрос все еще слишком общий, более практичным примером конечной машины было бы полезно.

Ответ 7

Глядя на ответ Бен Фойгта, я дал альтернативный ответ:

while (state = next_state()) {
        switch (state) {
                case foo:
                        /* handle foo, and finally: */
                        /* error is set but not bothered with here */ 
                        break;
                case bar:
                        /* handle bar, and finally: */
                        /* error is set but not bothered with here */
                        break;
                /* ...other cases... */
        }

        if (error) {
                /* do some cleanup, i.e. free() local heap requests, */
                /* adjust global state, and then: */
                return error;
        }
}

return ok;

Недостатком этого является то, что вы должны помнить, что после его обработки состояние может быть очищено, если есть ошибка. Похоже, что структура if может быть цепочкой if-else для обработки различных типов ошибок.

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

Ответ 8

Если вам нужен только код очистки, который может быть вызван из нескольких мест в вашей процедуре, и ему нужно получить доступ к локальным ресурсам, возможно, вместо этого использовать оператор lambda. Определите его перед логикой коммутатора и просто вызовите его там, где вам нужно очистить. Мне нравится идея по нескольким причинам: 1) Это круче, чем goto (и это всегда важно) 2) Вы получаете чистую инкапсуляцию логики без необходимости создавать внешний метод и передавать множество параметров, поскольку лямбда может обращаться к тем же локальным переменным, что и закрытие.

Ответ 9

Использование goto для очистки кода путем разбиения множественного вложения на цикл очень удобно, вместо того, чтобы устанавливать флаги и проверять их в каждой вложенности. Например, если вы не можете открыть файл, и вы обнаружите его глубоко в вложенности, вы можете просто goto очистить сегмент и закрыть файлы и свободные ресурсы. Вы можете увидеть примеры goto в источниках инструментов coreutilities.

Ответ 10

Общий принцип, которым я хотел бы следовать, заключается в том, что нужно, когда это возможно, попытаться написать код, поток и дизайн которого соответствует требованиям проблемной области ( "что должна делать программа" ). Языки программирования включают в себя структуры управления и другие функции, которые хорошо подходят для большинства, но не для всех проблемных областей. Такие структуры следует использовать, когда они соответствуют требованиям программы. В тех случаях, когда требования к программе не подходят для языковых функций, я предпочитаю сосредоточиться на написании кода, который выражает то, что нужно сделать программе, чем на то, чтобы подгонять код к шаблонам, которые, хотя они отвечают требованиям других приложений, действительно не соответствуют требованиям к написанию программы.

В некоторых случаях очень естественный способ перевода состояний машин в код, в случаях, когда подпрограмма будет иметь возможность не "выходить", пока конечный автомат не выполнит какую-либо форму заключения, должен иметь Метка goto представляет каждое состояние и использует операторы if и goto для состояний переходов. Если требуемые переходы состояний лучше подходят для других структур управления (например, циклы while), использование таких циклов будет лучше, чем операторы goto, а с помощью операторов switch могут выполняться определенные виды "адаптации" (например, с рутинной выполнением перехода состояния каждый раз, когда он выполнялся, а не требовать от него немедленного запуска до завершения) намного проще. С другой стороны, поскольку оператор switch на самом деле просто переодетый "goto", может быть проще просто использовать goto напрямую, чем использовать оператор switch для имитации.

Ответ 11

Я далеко за то, что стал слепым фанатиком no-goto, но я считаю, что в этом случае нужно использовать механизм, который явно предназначен для обработки ошибок.

Предполагаемый код С#, но его можно легко модифицировать для всех других языков соответствия (Java, C++):

try
{
    while (state = next_state()) 
    {
        switch (state) 
        {
            case foo:
                /* handle foo, and finally: */
                if (error) 
                    throw new Exception();

                break;
            case bar:
                /* handle bar, and finally: */
                if (error) 
                    throw new Exception();

                break;
                /* ...other cases... */
        }
    }

    return ok;
}
catch (Exception e)
{
    /* do some cleanup, i.e. free() local heap requests, adjust global state, and then: 
    */
    return error;
}

Честно говоря, я никогда не использовал goto, потому что я на самом деле никогда не нуждался в этом. В большинстве случаев всегда существует очевидное решение, которое решает проблему.

Имейте в виду, что существуют некоторые языковые конструкции, которые выполняют, скажем, контролируемый goto, например throw/catch/finally (очевидно), переключатель (который просто безоговорочно переходит к конкретному случаю) и циклы (очевидно). Эффективно, если, как представляется, существует необходимость в goto, в большинстве случаев фактическая потребность в конкретном использовании goto, которую уже предоставляет одна из существующих конструкций.

Тем не менее, я все еще поддерживаю мнение, что руководящие принципы кодирования не являются законами, и они не должны служить оправданием для отключения вашего мозга. Тот факт, что я не нашел действительного случая для использования goto, определенно не означает, что их нет. Если использование goto выражает намерение программиста более четко, чем любая другая конструкция, используйте его.