Как стать автором
Обновить

О вреде GOTO-фобии (с примерами на C)

Время на прочтение17 мин
Количество просмотров30K
Автор оригинала: Jakub Łukasiewicz

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

Каждая собака знает (уже мемородный) заголовок статьи Дейкстры Letters to the editor: go to statement considered harmful («О вреде оператора Go To») (изначально эта статья называлась A case against the goto statement). Но, как правило, забывают, в каком контексте была в 60-е написана эта статья. Ведь те вещи, которые сегодня воспринимаются как данность, тогда были в новинку.

Многие программисты учились своему ремеслу в мире, где оператор goto был основным инструментом, позволявшим контролировать поток управления. Даже в структурированных языках программист легко возвращался к выученным вредным привычкам и порочным методам. С другой стороны, сегодня складывается прямо противоположная ситуация: программисты не пользуются   goto, когда он уместен, а вместо этого злоупотребляют другими конструкциями. По иронии судьбы, от этого читать код становится только неудобнее. Это зацикленность на ЧТО СДЕЛАТЬ ("избавиться от goto"), а не на ЗАЧЕМ ("поскольку так код становится удобнее читать и поддерживать").

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

Сам по себе оператор "goto" не опасен — это языковая возможность, которая напрямую преобразуется в инструкции перехода, реализованные в машинном коде. «Goto» — точно как и указатели, перегрузка операторов и масса прочих «субъективных» зол — повсеместно ненавидят те, кто обжёгся на плохом программировании.   

Если вы считаете, что спагетти-код нельзя написать «без goto», могу прислать вам несколько милейших примеров, способных развеять подобное заблуждение ;)

Если использовать goto на коротких дистанциях, снабжая хорошо документированными метками, то он позволяет писать более эффективный, быстрый и чистый код, чем пересыпанный сложными флагами или другими конструкциями. Также goto может быть безопаснее и логичнее, чем его альтернативы. Инструкция «break» - это goto; инструкция «continue» - это goto. Все эти инструкции явно переносят точку выполнения кода.

Scott Robert Ladd

Конечно, ядро Linux стоит особняком, но даже если в таком строгом стандарте программирования как MISRA C (в редакции 2012 года) запрет на goto может быть смягчён с необходимого на рекомендуемый, то, думаю, в обычном коде вполне безопасно использовать goto так, как мы сочтём нужным.

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

Источники

Обработка ошибок/исключений и очистка

Образцовый пример работы с goto — в большинстве случаев приемлем, часто рекомендуется, а иногда без него даже решительно не обойтись. Применяя такой паттерн, получаем высококачественный код, так как работа алгоритма структурирована и чётко упорядочена, а ошибки и другие накладные расходы обрабатываются где-нибудь в сторонке, а не в основной линии развития программы. Альтернативы получаются не столь удобочитаемыми, а ещё в них сложно заметить, где именно основной код оказывается похоронен под ворохом проверки ошибок.

Из стандарта программирования SEI CERT языка C:

Для многих функций требуется выделять много ресурсов. Если выскальзывать и возвращаться где-нибудь в середине данной функции, не высвободив при этом всех ресурсов, то может возникнуть утечка в памяти. Распространённая ошибка — забыть высвободить один (или все) ресурс(ы) именно таким образом, поэтому цепочка goto – это простейший и самый аккуратный способ организовать выходы из функции, сохранив при этом правильный порядок высвобожденных ресурсов.

int* foo(int bar)
{
    int* return_value = NULL;

    if (!do_something(bar)) {
        goto error_didnt_sth;
    }
    if (!init_stuff(bar)) {
        goto error_bad_init;
    }
    if (!prepare_stuff(bar)) {
        goto error_bad_prep;
    }
    return_value = do_the_thing(bar);

error_bad_prep:
    clean_stuff();
error_bad_init:
    destroy_stuff();
error_didnt_sth:
    undo_something();

    return return_value;
}

Взятый наугад реальный пример из ядра Linux:

// SPDX-License-Identifier: GPL-2.0-or-later
/*
 * MMP Audio Clock Controller driver
 *
 * Copyright (C) 2020 Lubomir Rintel <lkundrak@v3.sk>
 */

static int mmp2_audio_clk_probe(struct platform_device *pdev)
{
	struct mmp2_audio_clk *priv;
	int ret;

	priv = devm_kzalloc(&pdev->dev,
			    struct_size(priv, clk_data.hws,
					MMP2_CLK_AUDIO_NR_CLKS),
			    GFP_KERNEL);
	if (!priv)
		return -ENOMEM;

	spin_lock_init(&priv->lock);
	platform_set_drvdata(pdev, priv);

	priv->mmio_base = devm_platform_ioremap_resource(pdev, 0);
	if (IS_ERR(priv->mmio_base))
		return PTR_ERR(priv->mmio_base);

	pm_runtime_enable(&pdev->dev);
	ret = pm_clk_create(&pdev->dev);
	if (ret)
		goto disable_pm_runtime;

	ret = pm_clk_add(&pdev->dev, "audio");
	if (ret)
		goto destroy_pm_clk;

	ret = register_clocks(priv, &pdev->dev);
	if (ret)
		goto destroy_pm_clk;

	return 0;

destroy_pm_clk:
	pm_clk_destroy(&pdev->dev);
disable_pm_runtime:
	pm_runtime_disable(&pdev->dev);

	return ret;
}

Альтернатива без goto № 1: вложенные if 

Недостатки:

  • вложение (антипаттерн "стрелка" )

  • потенциально возможно дублирование кода (см. приведённую в качестве примера функцию из Linux)

int* foo(int bar)
{
    int* return_value = NULL;

    if (do_something(bar)) {
        if (init_stuff(bar)) {
            if (prepare_stuff(bar)) {
                return_value = do_the_thing(bar);
            }
            clean_stuff();
        }
        destroy_stuff();
    }
    undo_something();

    return return_value;
}

Переписанный пример из ядра Linux

static int mmp2_audio_clk_probe(struct platform_device *pdev)
{
    // ...
    pm_runtime_enable(&pdev->dev);

    ret = pm_clk_create(&pdev->dev);
    if (!ret) {
        ret = pm_clk_add(&pdev->dev, "audio");
        if (!ret) {
            ret = register_clocks(priv, &pdev->dev);
            if (!ret) {
                pm_clk_destroy(&pdev->dev);
                pm_runtime_disable(&pdev->dev);
            }
        } else {
            pm_clk_destroy(&pdev->dev);
            pm_runtime_disable(&pdev->dev);
        }
    } else {
        pm_runtime_disable(&pdev->dev);
    }

    return ret; // в оригинале явно возвращался 0 
}

А вот Microsoft дарит нам милый пример такого «красивого» вложения (версия в архиве).

Альтернатива без goto № 2: если нет – тогда очищаем

Недостатки:

  • код дублируется

  • множество точек выхода

int* foo(int bar)
{
    int* return_value = NULL;

    if (!do_something(bar)) {
        undo_something();
        return return_value;
    }
    if (!init_stuff(bar)) {
        destroy_stuff();
        undo_something();
        return return_value;
    }
    if (!prepare_stuff(bar)) {
        clean_stuff();
        destroy_stuff();
        undo_something();
        return return_value;
    }

    clean_stuff();
    destroy_stuff();
    undo_something();

    return do_the_thing(bar);
}

Переписанный пример из ядра Linux

static int mmp2_audio_clk_probe(struct platform_device *pdev)
{
    // ...
    pm_runtime_enable(&pdev->dev);

    ret = pm_clk_create(&pdev->dev);
    if (ret) {
        pm_runtime_disable(&pdev->dev);
        return ret;
    }

    ret = pm_clk_add(&pdev->dev, "audio");
    if (ret) {
        pm_clk_destroy(&pdev->dev);
        pm_runtime_disable(&pdev->dev);
        return ret;
    }

    ret = register_clocks(priv, &pdev->dev);
    if (ret) {
        pm_clk_destroy(&pdev->dev);
        pm_runtime_disable(&pdev->dev);
        return ret;
    }

    return 0;
}

Альтернатива без goto № 3: флаги 

Недостатки:

  • дополнительные переменные

  • «каскадирующие» булевы значения

  • потенциально возможны вложения

  • потенциально возможны сложные булевы выражения

int* foo(int bar)
{
    int* return_value = NULL;

    bool flag_1 = false;
    bool flag_2 = false;
    bool flag_3 = false;

    flag_1 = do_something(bar);
    if (flag_1) {
        flag_2 = init_stuff(bar);
    }
    if (flag_2) {
        flag_3 = prepare_stuff(bar);
    }
    if (flag_3) {
        return_value = do_the_thing(bar);
    }

    if (flag_3) {
        clean_stuff();
    }
    if (flag_2) {
        destroy_stuff();
    }
    if (flag_1) {
        undo_something();
    }

    return return_value;
}

Переписанная функция mmp2_audio_clk_probe() не вполне чётко соответствует данному случаю, поэтому я решил, что лучше рассмотреть два варианта в альтернативе 3.5.

Альтернатива без goto № 3: флаг «пока и так сойдёт»

int foo(int bar)
{
    int return_value = 0;
    bool something_done = false;
    bool stuff_inited = false;
    bool stuff_prepared = false;
    bool oksofar = true;

    if (oksofar) {  // этот IF опционален (всегда execs), но включён сюда для полноты картины 
        if (do_something(bar)) {
            something_done = true;
        } else {
            oksofar = false;
        }
    }

    if (oksofar) {
        if (init_stuff(bar)) {
            stuff_inited = true;
        } else {
            oksofar = false;
        }
    }

    if (oksofar) {
        if (prepare_stuff(bar)) {
            stuff_prepared = true;
        } else {
            oksofar = false;
        }
    }

    // Делаем что нужно
    if (oksofar) {
        return_value = do_the_thing(bar);
    }

    // Очистка
    if (stuff_prepared) {
        clean_stuff();
    }
    if (stuff_inited) {
        destroy_stuff();
    }
    if (something_done) {
        undo_something();
    }

    return return_value;

Переписанный пример из ядра Linux

static int mmp2_audio_clk_probe(struct platform_device *pdev)
{
    // ...
    pm_runtime_enable(&pdev->dev);

    bool destroy_pm_clk = false;

    ret = pm_clk_create(&pdev->dev);
    if (!ret) {
        ret = pm_clk_add(&pdev->dev, "audio");
        if (ret) {
            destroy_pm_clk = true;
        }
    }
    if (!ret) {
        ret = register_clocks(priv, &pdev->dev);
        if (ret) {
            destroy_pm_clk = true;
        }
    }

    if (ret) {
        if (destroy_pm_clk) {
            pm_clk_destroy(&pdev->dev);
        }
        pm_runtime_disable(&pdev->dev);
        return ret;
    }

    return 0;
}

Переписанный пример из ядра Linux

static int mmp2_audio_clk_probe(struct platform_device *pdev)
{
    // ...
    pm_runtime_enable(&pdev->dev);

    bool destroy_pm_clk = false;
    bool disable_pm_runtime = false;

    ret = pm_clk_create(&pdev->dev);
    if (ret) {
        disable_pm_runtime = true;
    }
    if (!ret) {
        ret = pm_clk_add(&pdev->dev, "audio");
        if (ret) {
            destroy_pm_clk = true;
        }
    }
    if (!ret) {
        ret = register_clocks(priv, &pdev->dev);
        if (ret) {
            destroy_pm_clk = true;
        }
    }

    if (destroy_pm_clk) {
        pm_clk_destroy(&pdev->dev);
    }
    if (disable_pm_runtime) {
        pm_runtime_disable(&pdev->dev);
    }

    return ret;
}

Альтернатива без goto № 4: функции

Недостатки:

  • Становится больше объектов (это не только новые функции, но зачастую и структуры) "Не умножай сущностей сверх необходимости"

    • Более глубокий стек вызовов

  • Часто требуется передача контекста

    • Увеличение числа указателей, направленных на указатели

    • Дополнительные структуры, о которых уже упоминалось выше 

  • Фрагментированный код

    • Соблазн абстрагировать его без нужды

static inline int foo_2(int bar)
{
    int return_value = 0;
    if (prepare_stuff(bar)) {
        return_value = do_the_thing(bar);
    }
    clean_stuff();
    return return_value;
}

static inline int foo_1(int bar)
{
    int return_value = 0;
    if (init_stuff(bar)) {
        return_value = foo_2(bar);
    }
    destroy_stuff();
    return return_value;
}

int foo(int bar)
{
    int return_value = 0;
    if (do_something(bar)) {
        return_value = foo_1(bar);
    }
    undo_something();
    return return_value;
}

Переписанный пример из ядра Linux

static inline int mmp2_audio_clk_probe_3(struct platform_device* pdev)
{
    int ret = register_clocks(priv, &pdev->dev);
    if (ret) {
        pm_clk_destroy(&pdev->dev);
    }
    return ret;
}

static inline int mmp2_audio_clk_probe_2(struct platform_device* pdev)
{
    int ret = pm_clk_add(&pdev->dev, "audio");
    if (ret) {
        pm_clk_destroy(&pdev->dev);
    } else {
        ret = mmp2_audio_clk_probe_3(pdev);
    }
    return ret;
}

static inline int mmp2_audio_clk_probe_1(struct platform_device* pdev)
{
    int ret = pm_clk_create(&pdev->dev);
    if (ret) {
        pm_runtime_disable(&pdev->dev);
    } else {
        ret = mmp2_audio_clk_probe_2(pdev);
        if (ret) {
            pm_runtime_disable(&pdev->dev);
        }
    }
    return ret;
}

static int mmp2_audio_clk_probe(struct platform_device* pdev)
{
    // ...
    pm_runtime_enable(&pdev->dev);

    ret = mmp2_audio_clk_probe_1(pdev);

    return ret;
}

Альтернатива без goto № 5: злоупотребление циклами

Недостатки:

  • Половина недостатков, характерных goto

  • Половина недостатков, характерных другим альтернативам

  • Никаких преимуществ от двух вышеупомянутых стратегий

  • Никакой структурности

  • Создаёт цикл, который не крутится

  • Злоупотребление одной из языковых конструкций, только бы не использовать инструмент, который как раз нужен для этой задачи

  • Портится читаемость кода

  • Контринтуитивно, путано

  • Добавляются ненужные вложения

  • Больше строк кода

  • И не думайте поставить полноценный цикл где-нибудь внутри этой мешанины, даже если он целесообразен

int* foo(int bar)
{
    int* return_value = NULL;

    do {
        if (!do_something(bar)) break;
        do {
            if (!init_stuff(bar)) break;
            do {
                if (!prepare_stuff(bar)) break;
                return_value = do_the_thing(bar);
            } while (0);
            clean_stuff();
        } while (0);
        destroy_stuff();
    } while (0);
    undo_something();

    return return_value;
}

Переписанный пример из ядра Linux

static int mmp2_audio_clk_probe(struct platform_device *pdev)
{
    // ...
    pm_runtime_enable(&pdev->dev);

    do {
        ret = pm_clk_create(&pdev->dev);
        if (ret) break;

        do {
            ret = pm_clk_add(&pdev->dev, "audio");
            if (ret) break;

            ret = register_clocks(priv, &pdev->dev);
            if (ret) break;
        } while (0);
        pm_clk_destroy(&pdev->dev);
    } while (0);
    pm_runtime_disable(&pdev->dev);

    return ret;
}

Перезапуск/повторная попытка

Такие случаи особенно обычны в *nix-системах, когда приходится иметь дело с системными вызовами, возвращающими ошибку при прерывании сигналом. Также системный вызов устанавливает errno в EINTR, чтобы указать, что работа идёт нормально, просто была прервана. Разумеется, такие примеры не ограничиваются системными вызовами.

#include <errno.h>

int main()
{
retry_syscall:
    if (some_syscall() == -1) {
        if (errno == EINTR) {
            goto retry_syscall;
        }

        // обрабатываем настоящие ошибки
    }

    return 0;
}

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

Версия, в которой сокращены вложения:

#include <errno.h>

int main()
{
    int res;
retry_syscall:
    res = some_syscall();
    if (res == -1 && errno == EINTR) {
        goto retry_syscall;
    }

    "if (res == -1)"
        // обрабатываем настоящие ошибки
    }

    return 0;
}

Альтернатива без goto: цикл

Разумеется, можно воспользоваться циклом do {} while, указав условия в while:

#include <errno.h>

int main()
{
    int res;
    do {
        res = some_system_call();
    } while (res == -1 && errno == EINTR);

    if (res == -1) {
        // обрабатываем реальные ошибки
    }

    return 0;
}

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

Менее тривиальный пример

Рассмотрим такой пример: я хочу разнообразить общую монохромную тему на сайте и определить цвета для подсветки синтаксиса. Даже при простом парсинге, который делается с помощью kramdown (в данном случае ваш редактор кода определённо справится с задачей лучше), уже заметно, как немного проступают на общем фоне кода метки и инструкции goto. С другой стороны, флаги почти теряются на фоне других переменных.

Версия с goto  

#include <string.h>

enum {
    PKT_THIS_OPERATION,
    PKT_THAT_OPERATION,
    PKT_PROCESS_CONDITIONALLY,
    PKT_CONDITION_SKIPPED,
    PKT_ERROR,
    READY_TO_SEND,
    NOT_READY_TO_SEND
};

int parse_packet()
{
    static int packet_error_count = 0;

    int packet[16] = { 0 };
    int packet_length = 123;
    _Bool packet_condition = 1;
    int packet_status = 4;

    // получить пакет и т.д.

REPARSE_PACKET:
    switch (packet[0]) {
        case PKT_THIS_OPERATION:
            if (/* проблемная ситуация */) {
                goto PACKET_ERROR;
            }
            // ... обработать THIS_OPERATION
            break;

        case PKT_THAT_OPERATION:
            if (/* проблемная ситуация */) {
                goto PACKET_ERROR;
            }
            // ... обработать THAT_OPERATION
            break;

        // ...

        case PKT_PROCESS_CONDITIONALLY:
            if (packet_length < 9) {
                goto PACKET_ERROR;
            }
            if (packet_condition && packet[4]) {
                packet_length -= 5;
                memmove(packet, packet+5, packet_length);
                goto REPARSE_PACKET;
            } else {
                packet[0] = PKT_CONDITION_SKIPPED;
                packet[4] = packet_length;
                packet_length = 5;
                packet_status = READY_TO_SEND;
            }
            break;

        // ...

        default:
PACKET_ERROR:
            packet_error_count++;
            packet_length = 4;
            packet[0] = PKT_ERROR;
            packet_status = READY_TO_SEND;
            break;
    }

    // ...

    return 0;
}

Версия без goto

#include <string.h>

enum {
    PKT_THIS_OPERATION,
    PKT_THAT_OPERATION,
    PKT_PROCESS_CONDITIONALLY,
    PKT_CONDITION_SKIPPED,
    PKT_ERROR,
    READY_TO_SEND,
    NOT_READY_TO_SEND
};

int parse_packet()
{
    static int packet_error_count = 0;

    int packet[16] = { 0 };
    int packet_length = 123;
    _Bool packet_condition = 1;
    int packet_status = 4;

    // получить пакет и т.д.

    _Bool REPARSE_PACKET = true;
    _Bool PACKET_ERROR = false;

    while (REPARSE_PACKET) {
        REPARSE_PACKET = false;
        PACKET_ERROR = false;

        switch (packet[0]) {
            case PKT_THIS_OPERATION:
                if (/* проблемная ситуация */) {
                    PACKET_ERROR = true;
                    break;
                }
                // ... обработать THIS_OPERATION
                break;

            case PKT_THAT_OPERATION:
                if (/* проблемная ситуация */) {
                    PACKET_ERROR = true;
                    break;
                }
                // ... обработать THAT_OPERATION
                break;

                // ...

            case PKT_PROCESS_CONDITIONALLY:
                if (packet_length < 9) {
                    PACKET_ERROR = true;
                    break;
                }
                if (packet_condition && packet[4]) {
                    packet_length -= 5;
                    memmove(packet, packet+5, packet_length);
                    REPARSE_PACKET = true;
                    break;
                } else {
                    packet[0] = PKT_CONDITION_SKIPPED;
                    packet[4] = packet_length;
                    packet_length = 5;
                    packet_status = READY_TO_SEND;
                }
                break;

                // ...

            default:
                PACKET_ERROR = true;
                break;
        }

        if (PACKET_ERROR) {
            packet_error_count++;
            packet_length = 4;
            packet[0] = PKT_ERROR;
            packet_status = NOT_READY_TO_SEND;
            break;
        }
    }

    // ...

    return 0;
}

Общий код в инструкции switch

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

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

Так почему бы сразу не перейти к общему коду?

int foo(int v)
{
    // ...
    int something = 0;
    switch (v) {
        case FIRST_CASE:
            something = 2;
            goto common1;
        case SECOND_CASE:
            something = 7;
            goto common1;
        case THIRD_CASE:
            something = 9;
            goto common1;
common1:
            /* код, общий для ПЕРВОГО, ВТОРОГО и ТРЕТЬЕГО случаев */
            break;

        case FOURTH_CASE:
            something = 10;
            goto common2;
        case FIFTH_CASE:
            something = 42;
            goto common2;
common2:
            /* код, общий для ЧЕТВЁРТОГО и ПЯТОГО случаев */
            break;
    }
    // ...
}

Альтернатива без goto № 1: функции

Недостатки:

  • «Не умножай сущности сверх необходимого»

  • Код приходится читать снизу вверх, а не сверху вниз

  • Может потребоваться передача контекста

struct foo_context {
    int* something;
    // ...
};

static void common1(struct foo_context ctx)
{
    /* код, общий для ПЕРВОГО, ВТОРОГО и ТРЕТЬЕГО случаев */
}

static void common2(struct foo_context ctx)
{
    /* код, общий для ЧЕТВЁРТОГО и ПЯТОГО случаев */
}

int foo(int v)
{
    struct foo_context ctx = { NULL };
    // ...
    int something = 0;
    ctx.something = &something;

    switch (v) {
        case FIRST_CASE:
            something = 2;
            common1(ctx);
            break;
        case SECOND_CASE:
            something = 7;
            common1(ctx);
            break;
        case THIRD_CASE:
            something = 9;
            common1(ctx);
            break;

        case FOURTH_CASE:
            something = 10;
            common2(ctx);
            break;
        case FIFTH_CASE:
            something = 42;
            common2(ctx);
            break;
    }
    // ...
}

Альтернатива без goto № 2: if-ы

Можно обойтись без красивостей и заменить оператор switch на if

int foo(int v)
{
    // ...
    int something = 0;
    if (v == FIRST_CASE || v == SECOND_CASE || v == THIRD_CASE) {
        if (v == FIRST_CASE) {
            something = 2;
        } else if (v == SECOND_CASE) {
            something = 7;
        } else if (v == THIRD_CASE) { // здесь могло бы быть просто `else`
            something = 9;
        }
        /* код, общий для ПЕРВОГО, ВТОРОГО и ТРЕТЬЕГО случаев */
    } else if (v == FOURTH_CASE || v == FIFTH_CASE) {
        if (v == FOURTH_CASE) {
            something = 10;
        } else {
            something = 42;
        }
        /* код, общий для ЧЕТВЁРТОГО и ПЯТОГО случаев */
    }
    // ...
}

Альтернатива без goto № 3: попеременное использование if и (0)

А… нужно ли здесь что-либо комментировать?

Нельзя одновременно утверждать, что «попеременное использование – это хорошо» и что "goto – это плохо"!

int foo(int v)
{
    // ...
    int something = 0;
    switch (v) {
        case FIRST_CASE:
            something = 2;
      if (0) {
        case SECOND_CASE:
            something = 7;
      }
      if (0) {
        case THIRD_CASE:
            something = 9;
      }
            /* код, общий для ПЕРВОГО, ВТОРОГО и ТРЕТЬЕГО случаев */
            break;

        case FOURTH_CASE:
            something = 10;
      if (0) {
        case FIFTH_CASE:
            something = 42;
      }
            /* код, общий для ЧЕТВЁРТОГО и ПЯТОГО случаев */
            break;
    }
    // ...
}

Вложенные break, помеченные continue

Думаю, этот пример тоже не требуется дополнительно объяснять:

#include <stdio.h>

int main()
{
    for (int i = 1; i <= 5; ++i) {
        printf("outer iteration (i): %d\n", i);

        for (int j = 1; j <= 200; ++j) {
            printf("    inner iteration (j): %d\n", j);
            if (j >= 3) {
                break; // выход из внутреннего цикла, внешний цикл продолжает работу
            }
            if (i >= 2) {
                goto outer; // выход из внешнего цикла, переход непосредственно к "Done!"
            }
        }
    }
outer:

    puts("Done!");

    return 0;
}

Можно воспользоваться аналогичным механизмом при работе с continue.

В книге Beej's Guide to C Programming есть красивый пример, демонстрирующий,
как использовать эту технику наряду с очисткой:

for (...) {
        for (...) {
            while (...) {
                do {
                    if (some_error_condition) {
                        goto bail;
                    }
                    // ...
                } while(...);
            }
        }
    }

bail:
    // Здесь происходит очистка

Без goto пришлось бы проверять наличие флага ошибки во всех циклах, чтобы правильно пройти код.

Как покинуть цикл, выходя из инструкции switch

Аналогично, поскольку switch также использует ключевое слово break, именно из этой инструкции можно выйти, таким образом покинув цикл:

void func(int v)
{
    // ...

    while (1) {
        switch (v) {
            case SOME_V:
                // ...
                break;  // не выходит из цикла
            case STOP_LOOP:
                goto break_while;
        }
    }
break_while:

    // ...
}

Простые машины состояния

Далее приведён пример 1:1, недалеко ушедший от дословной математической нотации, позволяющей реализовать вышеприведённые автоматы:

_Bool machine(const char* c)
{
qA:
    switch (*(c++)) {
        case 'x': goto qB;
        case 'y': goto qC;
        case 'z': goto qA;
        default: goto err;
    }

qB:
    switch (*(c++)) {
        case 'x': goto qB;
        case 'y': goto qA;
        case '\0': goto F;
        default: goto err;
    }

qC:
    switch (*(c++)) {
        case 'x': goto qC;
        case 'z': goto qB;
        default: goto err;
    }

F:
    return true;

err:
    return false;
}

Переход в цикл событий

Да-да, я знаю, что предложение о переходе «в…» вас неприятно удивит. Тем не менее, в некоторых случаях вам может потребоваться сделать именно это.

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

#include <stdio.h>
#include <fancy_alloc.h>

int main()
{
    int* buf = NULL;
    size_t pos = 0;
    size_t sz = 8;

    int* temp;

    goto ALLOC;
    do {
        if (pos > sz) { // изменить размер массива
            sz *= 2;
ALLOC:      temp = arrayAllocSmart(buf, sz, pos);
            /* проверить наличие ошибок */
            buf = temp;
        }

        /* проделать что-нибудь с буфером */
    } while (checkQuit());

    return 0;

    /* обработка ошибок ... */
}

Альтернатива без goto № 1: сторожевой флаг

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

Недостатки — те же, что и обычно: вложения и необходимость отслеживать флаги.

#include <stdio.h>
#include <fancy_alloc.h>

int main()
{
    int* buf = NULL;
    size_t pos = 0;
    size_t sz = 8;

    int ret = 0

    _Bool firstIter = true;

    do {
        if (pos > sz || firstIter) { // изменить размер массива
            if (!firstIter) {
                sz *= 2;
                firstIter = false;
            }

            int* temp = arrayAllocSmart(buf, sz, pos);
            /* обработать ошибки ... */
            buf = temp;
        }

        /* проделать что-нибудь с буфером */
    } while (checkQuit());

    return 0;
}

Альтернатива без goto № 2: дублирование кода

Этот недостаток очевиден, так что обойдёмся без дальнейших комментариев.

#include <stdio.h>
#include <fancy_alloc.h>

int main()
{
    size_t pos = 0;
    size_t sz = 8;

    int* buf = arrayAllocSmart(NULL, sz, pos);
    /* обработка ошибок ... */

    do {
        if (pos > sz) { // изменить размер массива
            sz *= 2;
            int* temp = arrayAllocSmart(buf, sz, pos);
            /* обработать ошибки ... */
            buf = temp;
        }

        /* проделать что-нибудь с буфером */
    } while (checkQuit());

    return 0;
}

Оптимизация

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

Зачастую используются такие расширения как вычисленные goto 

В книге Биджа в качестве примера показана оптимизация хвостовых вызовов, к сожалению (о сожалении можно говорить только с педагогической точки зрения! Во всех остальных отношениях пример очень хорош) современные компиляторы легко оптимизируют такие простые вещи как факториал именно в такой ассемблерный код, который мы получили бы при оптимизации с использованием goto. С другой стороны, не каждому доступна такая роскошь, как современный оптимизирующий компилятор…

Структурированное программирование с использованием инструкций go to

Читайте тут: [ACM Digital Library]   [PDF]   [HTML]

Если я начал с Дейкстры, то естественно будет завершить Кнутом.

Почти каждый, кто положительно высказывается об инструкциях goto, ссылается на эту работу. И это справедливо! По сей день это один из самых полных источников на рассматриваемую тему (и настольная статья о goto). Пожалуй, некоторые из приведённых в ней примеров порядком устарели, некоторые проблемы не столь критичны, как на момент написания, но, тем не менее, это отличный материал.

Однако, есть вещь, которую мы здесь явно не артикулировали: из-за чего некоторые go to получаются плохими, а другие – приемлемыми. Дело в том, что внимание заостряется не на той проблеме: затрагивается объективный вопрос об устранении go to, а не важный субъективный вопрос о том, как структурировать программу. Выражаясь словами Джона Брауна, «Бросая наши мощнейшие интеллектуальные ресурсы на достижение призрачной цели писать программы без go to, мы сами себе помогли абстрагироваться от всех тех неподатливых и, возможно, нерешаемых проблем, с которыми в противном случае пришлось бы тягаться современному программисту». Написав эту длинную статью, я не хотел подлить масла в огонь споров об этих противоречиях, поскольку тема и так казалась слишком важной. Я ставил перед собой цель отложить эти противоречия в сторону и попытаться направить дискуссию в более плодотворное русло.  

Теги:
Хабы:
+115
Комментарии344

Публикации

Изменить настройки темы

Истории

Работа

Программист С
49 вакансий

Ближайшие события

Weekend Offer в AliExpress
Дата20 – 21 апреля
Время10:00 – 20:00
Место
Онлайн
Конференция «Я.Железо»
Дата18 мая
Время14:00 – 23:59
Место
МоскваОнлайн