2015-08-21 6 views
3

Прежде всего, если кто-то может изменить вопрос, чтобы сделать его более понятным, сделайте.Каковы некоторые хорошие способы обработки ошибок (очистка и прерывание) в функции, которая инициализирует несколько ресурсов в C?

Обычное явление в программировании на С имеет несколько ресурсов, которые должны быть инициализированы/выделены в определенном порядке. Каждый ресурс является предварительным условием для инициализации последующих. Если один из шагов не выполняется, оставшиеся ресурсы предыдущих шагов должны быть де-распределены. Идеально псевдокод (с использованием функции волшебно родовое чисто унобтаиниум clean_up_and_abort()) будет выглядеть примерно следующим образом:

err=alloc_a() 
if(err) 
    clean_up_and_abort() 

err=alloc_b() 
if(err) 
    clean_up_and_abort() 

err=alloc_c() 
if(err) 
    clean_up_and_abort() 

// ... 

profit() 

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

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

Цель состоит в том, чтобы надеяться, в конечном итоге, с набором нескольких предпочтительных методов решения этой проблемы.

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

ответ

2

4: Глобальные переменные, woohoo !!! Потому что все любят глобальные переменные, как они любят goto. Но если серьезно, если вы ограничиваете область видимости в области файлов (используя статическое ключевое слово), то это не так уж плохо. Боковое примечание: функция cleanup принимает/возвращает код ошибки без изменений, чтобы declutter код в init_function.

static A *a = NULL; 
static B *b = NULL; 
static C *c = NULL; 

uint32_t cleanup(uint32_t errcode) 
{ 
    if (c) 
     dealloc_c(&c); 
    if (b) 
     dealloc_b(&b); 
    if (a) 
     dealloc_a(&a); 

    return errcode; 
} 

uint32_t init_function(void) 
{ 
    if (alloc_a(&a) != SUCCESS) 
     return cleanup(INIT_FAIL_A); 

    if (alloc_b(&b) != SUCCESS) 
     return cleanup(INIT_FAIL_B); 

    if (alloc_c(&c) != SUCCESS) 
     return cleanup(INIT_FAIL_C); 

    profit(a,b,c); 
    return INIT_SUCCESS; 
} 

5: Поддельный OOP. Для тех, кто не может справиться с истиной (глобальные переменные действительно полезны в программах на C), вы можете использовать подход C++.C++ принимает все глобальные переменные, помещает их в структуру и называет их «member» переменных. И как-то это радует всех.

Фокус в том, чтобы передать указатель на структуру всем функциям в качестве первого аргумента. C++ делает это за кулисами, в C вы должны делать это явно. Я вызываю указатель that, чтобы избежать конфликтов/путаницы с this.

// define a class (uhm, struct) with status, a cleanup method, and other stuff as needed 
typedef struct stResources 
{ 
    char *status; 
    A *a; 
    B *b; 
    C *c; 
    void (*cleanup)(struct stResources *that); 
} 
stResources; 

// the cleanup method frees all resources, and frees the struct 
void cleanup(stResources *that) 
{ 
    if (that->c) 
     dealloc_c(&that->c); 
    if (that->b) 
     dealloc_b(&that->b); 
    if (that->a) 
     dealloc_a(&that->a); 

    free(that); 
} 

// the init function returns a pointer to the struct, or NULL if the calloc fails 
// the status member variable indicates whether initialization succeeded, NULL is success 
stResources *init_function(void) 
{ 
    stResources *that = calloc(1, sizeof(stResources)); 

    if (!that) 
     return NULL; 

    that->cleanup = cleanup; 

    that->status = "Item A is out to lunch"; 
    if (alloc_a(&that->a) != SUCCESS) 
     return that; 

    that->status = "Item B is never available when you need it"; 
    if (alloc_b(&that->b) != SUCCESS) 
     return that; 

    that->status = "Item C is being hogged by some other process"; 
    if (alloc_c(&that->c) != SUCCESS) 
     return that; 

    that->status = NULL; // NULL is success 
    return that; 
} 

int main(void) 
{ 
    // create the resources 
    stResources *resources = init_function(); 

    // use the resources 
    if (!resources) 
     printf("Buy more memory already\n"); 
    else if (resources->status != NULL) 
     printf("Uhm yeah, so here's the deal: %s\n", resources->status); 
    else 
     profit(resources->a, resources->b, resources->c); 

    // free the resources 
    if (resources) 
     resources->cleanup(resources); 
} 
+2

За отличную статью/учебник по неполным типам данных, инкапсуляцию, скрытие данных, динамическую связь/позднюю привязку и объектно-ориентированные подходы к динамическим структурам данных, см. [** Объектно-ориентированное программирование в ANSI-C **] (http://www.cs.rit.edu/~ats/books/ooc.pdf). Хотя это написано и требует достаточно глубоких знаний о C, это стоит усилий, чтобы усвоить материал. Он охватывает ряд тем, не включенных в большинство книг или учебников C. –

+1

благодарим вас за ввод! Ключевой идеей для них обоих является создание общей функции очистки, которая проверяет, был ли выделен каждый ресурс, о чем я не думал. Все эти разговоры о ООП в C напоминают мне о большой статье, которую я прочитал о внедрении исключений в C: http://www.on-time.com/ddj0011.htm. Выглядит красиво. Похоже, это удобный инструмент для разумно сложных встроенных проектов, но я не был достаточно храбр, чтобы еще попробовать его. – Dmitri

5

Три наиболее распространенных методов, которые я видел до сих пор:

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

uint32_t init_function() { 
    uint32_t erra, errb, errc, status; 
    A *a; 
    B *b; 
    C *c; 

    erra = alloc_a(&a); 
    if(erra) { 
     status = INIT_FAIL_A; 
    } else { 

     errb = alloc_b(&b); 
     if(errb) { 
      dealloc_a(&a); 
      status = INIT_FAIL_B; 
     } else { 

      errc = alloc_c(); 
      if(errc) { 
       dealloc_b(&b); 
       dealloc_a(&a); 
       status = INIT_FAIL_C; 
      } else { 

       profit(a,b,c); 
       status = INIT_SUCCESS; 

      } 
     } 
    } 
    // Single return. 
    return status; 
} 

2: Несколько возвращается. Это мой предпочтительный метод прямо сейчас. Логику легко следовать, но она по-прежнему опасна, потому что код очистки должен быть дублирован, и легко забыть освободить что-то в одном из разделов очистки.

uint32_t init_function() { 
    uint32_t err; 
    A *a; 
    B *b; 
    C *c; 

    err = alloc_a(&a); 
    if(err) { 
     return INIT_FAIL_A; 
    } 

    err = alloc_b(&b); 
    if(err) { 
     dealloc_a(&a); 
     return INIT_FAIL_B; 
    } 

    err = alloc_c(&c); 
    if(err) { 
     dealloc_b(&b); 
     dealloc_a(&a); 
     return INIT_FAIL_C; 
    } 

    profit(a,b,c); 
    return INIT_SUCCESS; 
} 

3: GOTO. Многие люди не любят goto по принципу, но это один из стандартных аргументов для правильного использования goto в программировании на C. Преимущество состоит в том, что трудно забыть о шаге очистки, и копирование не происходит.

uint32_t init_function() { 
    uint32_t status; 
    uint32_t err; 
    A *a; 
    B *b; 
    C *c; 

    err = alloc_a(&a); 
    if(err) { 
     status = INIT_FAIL_A; 
     goto LBL_FAIL_A; 
    } 

    err = alloc_b(&b); 
    if(err) { 
     status = INIT_FAIL_B; 
     goto LBL_FAIL_B; 
    } 

    err = alloc_c(&c); 
    if(err) { 
     status = INIT_FAIL_C; 
     goto LBL_FAIL_C; 
    } 

    profit(a,b,c); 
    status = INIT_SUCCESS; 
    goto LBL_SUCCESS; 

    LBL_FAIL_C: 
    dealloc_b(&b); 

    LBL_FAIL_B: 
    dealloc_a(&a); 

    LBL_FAIL_A: 
    LBL_SUCCESS: 

    return status; 
} 

Что-нибудь еще я не упоминал?

+2

Рад, что вы упомянули Goto и представили чистый пример. Для меня это самый читаемый и простой в обслуживании, за исключением того, что я считаю, что одна метка GOTO для ошибок и одна для успеха чище. В этом случае вы отключите ошибку и ответьте на единую метку ошибки. Вложенные операторы if могут быть очень уродливыми, а множественные возвраты также могут быть проблематичными. –

+1

'goto' не мертв и во многих случаях обеспечивает наиболее эффективный способ управления ветвью в ситуациях, когда имеется несколько условных блоков кода. Он может устранить дублирование кода и, прежде всего - полностью читаемый. Почему вы больше не видите это на практике, это тайна. Если вы посмотрите на стандартный исходный код функции C-библиотеки, вы быстро найдете его фаворитом. –

+0

Моя старшая школа началась с 100 человек. В то время мы могли в значительной степени делать все, что хотели, и почему-то всем удавалось избегать неприятностей, в то время как они хорошо учились. После того, как он вырос до 500, «один парень» сделал «ту самую глупую вещь», которая заставила администрацию написать толстую книгу правил и значительно ограничить свободу каждого. Я думаю, что это аналогично тому, как goto рассматривался в мире программирования. Все началось нормально. Затем некоторые люди совершили отвратительные преступления, используя заявления GOTO, Дейкстра написал эссе, осуждающее его, и стало правилом избегать gotos ... – Dmitri