LINUX.ORG.RU

C: освобождение ресурсов и обработка ошибок?


0

0

Есть функция (C, std=c99), которая работает внутри себя с динамическими данными (выделение памяти, работа с указателями и т.д.) и практически на каждом шаге проверяет корректность работы:

  if (cond1) {  // e.g., malloc() returned NULL
     free(val1);
     free(val2);
     free_struct(s_val1);

     return false;
  }
  ...
  if (cond2) { // e.g., realloc() returned NULL
     free(val1);
     ...
     free(val10);
     free_struct(s_val);
   
     return false;
  }

Откровенно говоря, меня эта вербозность несколько анноит, однако, отказываться от проверки ошибок в runtime тоже не хочется. Хотелось бы узнать, как принято решать такие usecases -- думаю, что они не такие уж и редкие? На ум приходит либо использование меток (using goto), или  просто делать банальный stdlib.h::abort() вместо освобождения ресурсов, однако, мне кажется это не совсем правильно (да и мой исходный вариант, возможно тоже). Поделитесь опытом, пожалуйста? :)

Спасибо.
anonymous

Sorry за кривое оформление. Вот сам вопрос:

>>> Откровенно говоря, меня эта вербозность несколько анноит, однако, отказываться от проверки ошибок в runtime тоже не хочется. Хотелось бы узнать, как принято решать такие usecases -- думаю, что они не такие уж и редкие? На ум приходит либо использование меток (using goto), или просто делать банальный stdlib.h::abort() вместо освобождения ресурсов, однако, мне кажется это не совсем правильно (да и мой исходный вариант, возможно тоже). Поделитесь опытом, пожалуйста? :)

anonymous
()
Ответ на: комментарий от anonymous

> В "C" для этого применяется goto

> ++ и макрофы :%)

Хм... А изначальный вариант (мой) с кучей проверок насколько корректен? Просто меня нет опыта (в том числе и положительного) в применении goto в C программах.

anonymous
()

вариант 1:

func()
{
    p = malloc();
    if (p) {
        fd = open();
        if (fd >= 0) {
            fd2 = open();
            if (fd2 >= 0) {
                return OK;
            }
            close(fd);
        }
        free(p);
    }
    return ВСЁПЛОХО;
}

минус: код уползает вправо.

вариант 2:

func()
{
    p = malloc();
    if (!p)
        goto l1;
    fd = open();
    if (fd < 0)
        goto l2;
    fd2 = open();
    if (fd2 < 0)
        goto l3;
    return OK;
l3:
    close(fd);
l2:
    free(p)
l1:
    return ВСЁПЛОХО;
}

минус: по сравнению с 1 не очень наглядно IMHO.

Вместо кучи меток можно попытаться обойтись одной и проверять
переменные на инициализированность:

err:
    if (fd >=0) close(fd);
    if (p) free(p);

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

execve
()
Ответ на: комментарий от execve

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

Для каждой функции в программе (с учетом отсутствия глобальных переменных) несколько надоест писать вспомогательные функции, imho. Вот первый вариант, действительно: и прост, и интересен. Спасибо :).

anonymous
()
Ответ на: комментарий от execve

>func()
{
p = malloc();
if (!p)
goto l1;
fd = open();
if (fd < 0)
goto l2;
fd2 = open();
if (fd2 < 0)
goto l3;
return OK;
l3:
close(fd);
l2:
free(p)
l1:
return ВСЁПЛОХО;
}

>минус: по сравнению с 1 не очень наглядно IMHO.

если l1 заменить как out_err,

l2 - out_free
l3 - out_close_free, то будет более наглядно,
а вообще
хорошо написанных програм на С с исходным кодом,
хотя и мало, но все-таки они существуют, например,
часть ядра в поддиректории kernel

fghj ★★★★★
()
Ответ на: комментарий от anonymous

> Для каждой функции в программе (с учетом отсутствия глобальных переменных) несколько надоест писать вспомогательные функции, imho. Вот первый вариант, действительно: и прост, и интересен. Спасибо :).

Обычно такая функция - это конструктор чего-то большого. И возвращает она как правило указатель на структуру с инициализированными переменными, который и передаётся в функцию-деструктор.

Так что ни кучи функций, ни глобальных переменных не надо.

execve
()
Ответ на: комментарий от fghj

> если l1 заменить как out_err,

Неудобно глазами скакать туда-сюда. Ну и можно пропустить что-то - в первом случае за счёт выделения синтаксисом это сделать сложнее.

execve
()
Ответ на: комментарий от execve

лучше так:

int func(...)
{
  void *p1 = 0;
  void *p2 = 0;
  int fd = -1;
  int retval = -1;

  ...
  if (!(p1 = malloc(...))) goto cleanup;
  ...
  if (!(p2 = malloc(...))) goto cleanup;
  ...
  if ((fd = open(...)) < 0) goto cleanup;

  // а здесь зачем-то надо было явно память освободить:
  if (...) {
    free(p1); p1 = 0;
  }
  // а здесь мы отдали fd в глобальную структуру
  if (...) {
    global_data->fd = fd; fd = -1;
  }

cleanup:
  if (p1) free(p1);
  if (p2) free(p2);
  if (fd >= 0) close(fd);
  return retval;
}

плюсы: одна метка выхода, ресурс всегда будет освобожден и только один раз

anonymous
()

Всатавлю свои 2 копейки: ИМХО, способ 2, приведенный execve - это то, что надо. Есть еще pool-based техники, основанные на использовании пулов ресурсов в стиле APR.

tailgunner ★★★★★
()

Господа, а если malloc() вернул NULL, имеет ли вообще смысл дальнейшего выполнения программы?

anonymous
()
Ответ на: комментарий от anonymous

>Господа, а если malloc() вернул NULL, имеет ли вообще смысл >дальнейшего выполнения программы?

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

fghj ★★★★★
()
Ответ на: комментарий от anonymous

> Господа, а если malloc() вернул NULL, имеет ли вообще смысл дальнейшего выполнения программы?

Зависит от программы. Представь, что у тебя есть 2 алгоритма: требующий 1Gb памяти быстрый и требующий 100Mb медленный. В этом случае неудача с выделением 1Gb вобщем-то не смертельна.

execve
()
Ответ на: комментарий от execve

>еудобно глазами скакать туда-сюда. Ну и можно пропустить что-то - в >первом случае за счёт выделения синтаксисом это сделать сложнее.

"неудобно" и т.д. это скорее вопрос привычки,
чем реальное преимущество,
достаточно написать просто освобождение ресурсов,
в обратном порядке их выделению, а потом раставить метки,

тем более если ресурсов слишком много выделяется освобождается,
это скорее всего проблема архитектуры,

метод с goto имеет преимущество, что это стандартный трюк для С,
что важно с точки зрения поддержки кода не вами.


fghj ★★★★★
()
Ответ на: комментарий от anonymous

>плюсы: одна метка выхода, ресурс всегда будет освобожден и только >один раз

минусы, "if" лишний, мы знаем что выделено успешно, а что нет,
а если такого типа минимальный выигрыши не важны,
зачем писать на С

fghj ★★★★★
()
Ответ на: комментарий от execve

>Представь, что у тебя есть 2 алгоритма: требующий 1Gb памяти быстрый >и требующий 100Mb медленный. В этом случае неудача с выделением 1Gb >вобщем-то не смертельна.

или как было в gimp(или есть?),
открываешь большой файл, памяти не хватает и gimp падает,
потому что g_malloc ихняя обертка над malloc просто вызывала(вызывает?) exit в случае неудачи.

fghj ★★★★★
()
Ответ на: комментарий от fghj

> минусы, "if" лишний, мы знаем что выделено успешно, а что нет,

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

Зато, если ставить метку на каждое освобождение ресурса и меток будет более чем 2-3, запутаться в них будет элементарно.

> а если такого типа минимальный выигрыши не важны, зачем писать на С

гм... тяжелый случай... ;) повторяйте как мантру: "premature optimization is the root of all evil" ;)

anonymous
()
Ответ на: комментарий от anonymous

>гм... тяжелый случай... ;) повторяйте как мантру: "premature >optimization is the root of all evil" ;)

На C сейчас разумно писать что-либо низкоуровневое,
там это не слишком ранняя оптимизация, а просто стиль написания.

А вообще "usage of C language is root of all evil".

fghj ★★★★★
()

ну например:

--- foo.c ---
#include <stdlib.h>

struct cont {
    void *foo1, *foo2, *foo3;
    enum {
        INIT_NONE,
        INIT_FOO1,
        INIT_FOO2,
        INIT_FOO3
    } phase;
};

void cleanup(struct cont *cont);

void
init(void)
{
    struct cont cont = { .phase = INIT_NONE };

    cont.foo1 = malloc(1);
    if (!cont.foo1) goto out;
    cont.phase = INIT_FOO1;

    cont.foo2 = malloc(2);
    if (!cont.foo2) goto out;
    cont.phase = INIT_FOO2;

    cont.foo3 = malloc(3);
    if (!cont.foo3) goto out;
    cont.phase = INIT_FOO3;

    // ......

    return;

out :
    cleanup(&cont);
}

void
cleanup(struct cont *cont)
{
    switch (cont->phase) {
    case INIT_FOO3 :
        free(cont->foo3);
    case INIT_FOO2 :
        free(cont->foo2);
    case INIT_FOO1 :
        free(cont->foo1);
    default :
        break;
    }
}
--- foo.c ---

// wbr

klalafuda ★☆☆
()
Вы не можете добавлять комментарии в эту тему. Тема перемещена в архив.