main

Обработка ошибок в си

Навеяно вознёй со своим проектом, и вот этим срачем "как правильно закрывать файл" на 18 страниц. На полноту охвата вопроса не претендую, только то что видел и использовал сам.

Будем рассматривать на примере выделения памяти для сложного ресурса, сферичной структуры в вакууме с несколькими членами, также выделяемыми через *alloc(). Вот такой:

struct foo {
  int id;
  char *data1;
  char *data2;
};

Сразу оговорюсь, что я считаю использование функций-аллокаторов "всего и сразу" - плохой практикой.

Лучшим способом мне кажется подход, когда сначала выделяется сама структура, а в функции, скажем, bool foo_init(struct *foo) - выделяются все её компоненты (и там же освобождаются при неудаче). Т.е. ресурсы не должны "всплывать" вверх по стеку через return.

Это позволяет писать код однообразно с тем, когда такая структура выделяется просто на стеке.

Лестница if'ов

Он же "дийкстра-стиль".

struct *foo
new_foo() {
  struct *t = NULL;
  t = calloc(1, sizeof(foo));
  if (t) {
    t->data1 = calloc(48, sizeof(char));
    if (t->data1) {
      t->data2 = calloc(64, sizeof(char));
      if (t->data2) {
        return t;
      }
      free(t->data1);
    }
    free(t);
  }
  return NULL;
}

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

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

Магический switch

Вариация предыдущего пункта.

struct *foo
new_foo() {
  struct *t = NULL;
  int stage = 0;
  t = calloc(1, sizeof(foo));
  if (t) {
    stage++;
    t->data1 = calloc(48, sizeof(char));
    if (t->data1) {
      stage++
      t->data2 = calloc(64, sizeof(char));
      if (t->data2) {
        return t;
      }
    }
  }
  switch (stage) {
    case 2 : free(t->data1);
    case 1 : free(t);
    default :
      break;
  }
  return NULL;
}

Эта техника называется "на что только не идут люди, чтобы goto не использовать". Используется тот факт, что в case без break выполнение продолжается на следующее условие.

Можно не int, а enum, тогда stage станет читаемым, и можно обойтись без default, компилятор ругнётся, если мы перечислили не все означенные case.

goto cleanup

Великий и ужасный goto. Широко распространённая практика, используется, например, в том же openssl1 или ядре linux.

struct *foo
new_foo() {
  struct *t = NULL;
  t = calloc(1, sizeof(foo));
  if (!t)
    goto cleanup;
  t->data1 = calloc(48, sizeof(char));
  t->data2 = calloc(64, sizeof(char));
  if (!t->data1 || !t->data2))
    goto cleanup;
  return t;
  /* но кажется что-то пошло не так... */
  cleanup:
  if (t) {
    free(t->data1);
    free(t->data2);
  }
  free(t);
  return NULL;

Тут есть одна хитрость: все alloc() рекомендуется собрать в кучу вынести вниз, после всех проверок насколько возможно. Тогда в начале функции до первого alloc() можно использовать return, а не goto.

do-false

Вариант предыдущего пункта. Уходим от использования goto через более ограниченный break.

struct *foo
new_foo() {
  struct *t = NULL;
  do {
    t = calloc(1, sizeof(foo));
    if (!t)
      break;
    t->data1 = calloc(48, sizeof(char));
    t->data2 = calloc(64, sizeof(char));
    if (!t->data1 || !t->data2))
      break;
    return t;
  } while (0);

  if (t) {
    free(t->data1);
    free(t->data2);
  }
  free(t);
  return NULL;
}

Более прямолинеен чем предыдущий вариант, легче читать.

use the stack, Luke!

В случае, если с такой структурой надо работать в той же функции - можно сэкономить на вызовах free(), использовав alloca(). Пишу на правах КО.

Magick-way

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

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

if (status == MagickPass)
  status = MagickNormalizeImage(wand);
if (status == MagickPass)
    status = MagickThresholdImage(wand, 50.0 * (MaxRGB / 100));
if (status == MagickPass)
  status = MagickSetImageType(wand, BilevelType);
if (status == MagickPass)
  status = MagickSetImageFormat(wand, "MONO");
/* длинная простыня подобного */
if (status != MagickPass) {
  /* где-то выше сфейлилось, врубаем panic-mode и жарим карри */
  description = MagickGetException(wand, &severity);
  fprintf(stderr, "%03d %.1024s\n", severity, description);
  DestroyMagickWand(wand);
  return NULL;
}

  1. хотя его нельзя назвать образцом для подражания. ↩