Допустимое использование goto для управления ошибками в C? - PullRequest
85 голосов
/ 25 апреля 2009

Этот вопрос на самом деле является результатом интересного обсуждения на сайте software.reddit.com. В основном это сводится к следующему коду:

int foo(int bar)
{
    int return_value = 0;
    if (!do_something( bar )) {
        goto error_1;
    }
    if (!init_stuff( bar )) {
        goto error_2;
    }
    if (!prepare_stuff( bar )) {
        goto error_3;
    }
    return_value = do_the_thing( bar );
error_3:
    cleanup_3();
error_2:
    cleanup_2();
error_1:
    cleanup_1();
    return return_value;
}

Использование goto здесь, кажется, лучший путь, в результате чего получается самый чистый и эффективный код из всех возможных, или, по крайней мере, мне так кажется. Цитировать Стива Макконнелла в Код завершен :

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

Другая поддержка этого подхода содержится в книге Драйверы устройств Linux , в этом разделе .

Что ты думаешь? Является ли этот случай допустимым для goto в C? Вы бы предпочли другие методы, которые производят более запутанный и / или менее эффективный код, но избегают goto?

Ответы [ 14 ]

0 голосов
/ 06 июня 2012

Мы используем библиотеку Daynix CSteps в качестве другого решения проблемы " goto " в функциях init. См. здесь и здесь .

0 голосов
/ 17 ноября 2011

Я предпочитаю использовать технику, описанную в следующем примере ...

struct lnode *insert(char *data, int len, struct lnode *list) {
    struct lnode *p, *q;
    uint8_t good;
    struct {
            uint8_t alloc_node : 1;
            uint8_t alloc_str : 1;
    } cleanup = { 0, 0 };

    // allocate node.
    p = (struct lnode *)malloc(sizeof(struct lnode));
    good = cleanup.alloc_node = (p != NULL);

    // good? then allocate str
    if (good) {
            p->str = (char *)malloc(sizeof(char)*len);
            good = cleanup.alloc_str = (p->str != NULL);
    }

    // good? copy data
    if(good) {
            memcpy ( p->str, data, len );
    }

    // still good? insert in list
    if(good) {
            if(NULL == list) {
                    p->next = NULL;
                    list = p;
            } else {
                    q = list;
                    while(q->next != NULL && good) {
                            // duplicate found--not good
                            good = (strcmp(q->str,p->str) != 0);
                            q = q->next;
                    }
                    if (good) {
                            p->next = q->next;
                            q->next = p;
                    }
            }
    }

    // not-good? cleanup.
    if(!good) {
            if(cleanup.alloc_str)   free(p->str);
            if(cleanup.alloc_node)  free(p);
    }

    // good? return list or else return NULL
    return (good? list: NULL);

}

источник: http://blog.staila.com/?p=114

0 голосов
/ 29 мая 2010

Я думаю, что вопрос здесь ошибочен в отношении данного кода.

Рассмотрим:

  1. do_something (), init_stuff () и prepare_stuff (), кажется, знают, что они потерпели неудачу, так как в этом случае они возвращают либо false, либо nil.
  2. Ответственность за настройку состояния, по-видимому, лежит на этих функциях, поскольку в foo () не устанавливается состояние непосредственно.

Следовательно: do_something (), init_stuff () и prepare_stuff () должны делать свою собственную очистку . Наличие отдельной функции cleanup_1 (), которая очищается после do_something (), нарушает философию инкапсуляции. Это плохой дизайн.

Если они сделали свою собственную очистку, то foo () становится довольно простым.

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

0 голосов
/ 25 апреля 2009

Сдается мне, что cleanup_3 должен выполнить очистку, а затем вызвать cleanup_2. Точно так же cleanup_2 должен выполнить очистку, затем вызвать cleanup_1. Похоже, что в любое время, когда вы делаете cleanup_[n], требуется cleanup_[n-1], поэтому ответственность за метод должна лежать на нем (так что, например, cleanup_3 никогда не может быть вызван без вызова cleanup_2 и возможной утечки) .)

При таком подходе вместо gotos вы просто вызываете процедуру очистки, а затем возвращаете.

Подход goto не является неправильным или плохим , однако, просто стоит отметить, что это не обязательно самый «чистый» подход (ИМХО).

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

...