Есть ли лучший способ обработки ошибок в стиле C? - PullRequest
11 голосов
/ 15 апреля 2011

Я пытаюсь выучить C, написав простой парсер / компилятор.До сих пор это был очень поучительный опыт, однако из-за сильного опыта в C # у меня возникли некоторые проблемы с адаптацией - в частности, из-за отсутствия исключений.

Теперь я прочитал Очиститель, большеэлегантный, и труднее узнать , и я согласен с каждым словом в этой статье;В моем коде на C # я по возможности не выкидываю исключения, однако теперь, когда я столкнулся с миром, в котором не могу выбрасывать исключения, моя обработка ошибок полностью затопляет понятную и простую для чтения логикумоего кода.

В данный момент я пишу код, который должен быстро сбоить , если есть проблема, и она также потенциально глубоко вложена - я остановился на обработке ошибокшаблон, посредством которого функции «Get» возвращают NULL при ошибке, а другие функции возвращают -1 при ошибке.В обоих случаях вызывающая сбой функция вызывает NS_SetError(), поэтому все, что нужно сделать вызывающей функции, - это очистить и немедленно вернуться к ошибке.

Моя проблема в том, что число операторов if (Action() < 0) return -1;, которые яЯ имею дело с головой - это очень повторяется и полностью затемняет основную логику.Я закончил тем, что создал себе простой макрос, чтобы попытаться улучшить ситуацию, например:

#define NOT_ERROR(X)    if ((X) < 0) return -1

int NS_Expression(void)
{
    NOT_ERROR(NS_Term());
    NOT_ERROR(Emit("MOVE D0, D1\n"));

    if (strcmp(current->str, "+") == 0)
    {
        NOT_ERROR(NS_Add());
    }
    else if (strcmp(current->str, "-") == 0)
    {
        NOT_ERROR(NS_Subtract());
    }
    else
    {
        NS_SetError("Expected: operator");
        return -1;
    }
    return 0;
}

Каждая из функций NS_Term, NS_Add и NS_Subtract делает NS_SetError() ивернуть -1 в случае ошибки - это лучше , но все равно кажется, что я злоупотребляю макросами и не допускаю очистки (некоторые функции, в частности функции Get, которые возвращаютуказатель, он более сложный и требует запуска кода очистки).

В целом кажется, что я что-то упустил - несмотря на то, что обработку ошибок таким способом предположительно легче распознать,Многие из моих функций я действительно пытаюсь определить, правильно ли обрабатываются ошибки:

  • Некоторые функции возвращают NULL при ошибке
  • Некоторые функции возвращают < 0об ошибке
  • Некоторые функции никогда не выдают ошибку
  • Мои функции делают NS_SetError(), но многие другие функции этого не делают.

Есть ли лучшетак, что я могу структурировать свой функционалили у всех остальных тоже есть эта проблема?

Имеет также Get функции (которые возвращают указатель на объект), возвращающие NULL в случае ошибки, хорошая идея, или это просто сбивает с толку моюобработка ошибок?

Ответы [ 10 ]

13 голосов
/ 15 апреля 2011

Это большая проблема, когда вам приходится повторять один и тот же код завершения перед каждым return из-за ошибки. В таких случаях широко принято использовать goto:

int func ()
{
  if (a() < 0) {
    goto failure_a;
  }

  if (b() < 0) {
    goto failure_b;
  }

  if (c() < 0) {
    goto failure_c;
  }

  return SUCCESS;

  failure_c:
  undo_b();

  failure_b:
  undo_a();

  failure_a:
  return FAILURE;
}

Вы даже можете создать свои собственные макросы, чтобы сэкономить при печати, что-то вроде этого (хотя я не проверял это):

#define CALL(funcname, ...) \
  if (funcname(__VA_ARGS__) < 0) { \ 
    goto failure_ ## funcname; \
  }

В целом, это гораздо более чистый и менее избыточный подход, чем обычная обработка:

int func ()
{
  if (a() < 0) {
    return FAILURE;
  }

  if (b() < 0) {
    undo_a();
    return FAILURE;
  }

  if (c() < 0) {
    undo_b();
    undo_a();
    return FAILURE;
  }

  return SUCCESS;
}

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

if (a() < 0 || b() < 0 || c() < 0) {
  return FAILURE;
}

Поскольку || является оператором короткого замыкания, вышеприведенное заменит три отдельных if. Также рассмотрите возможность использования цепочки в операторе return:

return (a() < 0 || b() < 0 || c() < 0) ? FAILURE : SUCCESS;
6 голосов
/ 15 апреля 2011

Один из методов очистки - использовать цикл while, который никогда не будет повторяться. Это дает вам goto без использования goto.

#define NOT_ERROR(x) if ((x) < 0) break;
#define NOT_NULL(x) if ((x) == NULL) break;

// Initialise things that may need to be cleaned up here.
char* somePtr = NULL;

do
{
    NOT_NULL(somePtr = malloc(1024));
    NOT_ERROR(something(somePtr));
    NOT_ERROR(somethingElse(somePtr));
    // etc

    // if you get here everything's ok.
    return somePtr;
}
while (0);

// Something went wrong so clean-up.
free(somePtr);
return NULL;

Вы теряете уровень отступа.

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

4 голосов
/ 15 апреля 2011

Возможно, вам не понравится это слышать, но C-способ делать исключения - с помощью оператора goto . Это одна из причин, по которой он есть в языке.

Другая причина в том, что goto является естественным выражением реализации конечного автомата. Какую общую задачу программирования лучше всего представить конечным автоматом? Лексический анализатор. Посмотрите на вывод из lex когда-нибудь. GOTOS.

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

3 голосов
/ 16 апреля 2011

Краткий ответ: пусть ваши функции возвращают код ошибки, который не может быть допустимым значением - и всегда проверяйте возвращаемое значение. Для функций, возвращающих указатели, это NULL. Для функций, возвращающих неотрицательное значение int, это отрицательное значение, обычно -1 и т. Д. ...

Если каждое возможное возвращаемое значение также является действительным значением, используйте вызов по ссылке:

int my_atoi(const char *str, int *val)
{
        // convert str to int
        // store the result in *val
        // return 0 on success, -1 (or any other value except 0) otherwise
}


Проверка возвращаемого значения каждой функции может показаться утомительной, но именно так ошибки обрабатываются в C. Рассмотрим функцию nc_dial () . Все, что он делает, это проверяет свои аргументы на достоверность и устанавливает сетевое соединение, вызывая getaddrinfo (), socket (), setsockopt (), bind () / listen () или connect (), наконец освобождая неиспользуемые ресурсы и обновляя метаданные. Это может быть сделано примерно в 15 строк. Однако функция имеет около 100 строк из-за проверки ошибок. Но это так в Си. Как только вы к этому привыкнете, вы можете легко замаскировать проверку ошибок в своей голове.

Кроме того, нет ничего плохого в множественном числе if (Action() == 0) return -1;. Наоборот: это обычно признак осторожного программиста. Хорошо быть осторожным.

И в качестве последнего комментария: не используйте макросы для чего-либо, кроме определения значений , если вы не можете оправдать их использование, когда кто-то указывает пистолетом на вашу голову. В частности, никогда не используйте операторы потока управления в макросах: это приводит в замешательство бедного парня, который должен поддерживать ваш код через 5 лет после вашего ухода из компании. Нет ничего плохого в if (foo) return -1;. Это просто, чисто и очевидно до такой степени, что вы не можете сделать лучше.

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

3 голосов
/ 15 апреля 2011

Помимо goto, стандарт C имеет еще одну конструкцию, обеспечивающую исключительное управление потоком setjmp/longjmp.Преимущество состоит в том, что вы можете легко разбить множественные вложенные управляющие операторы по сравнению с break, как это было предложено кем-то, и в дополнение к тому, что предоставляет goto, имеет индикатор состояния, который может кодировать причину того, что пошло не так.

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

if (bla) NOT_ERROR(X);
else printf("wow!\n");

пойдет в корне неправильно.Вместо этого я бы использовал что-то вроде

#define NOT_ERROR(X)          \
  if ((X) >= 0) { (void)0; }  \
  else return -1

.

3 голосов
/ 15 апреля 2011

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

Большинство крупных C-сред, которые я вижу всегда возвращают статус и "возвращать "значения по ссылке (это случай WinAPI и многих API C Mac OS).Вы хотите вернуть bool?

StatusCode FooBar(int a, int b, int c, bool* output);

Вы хотите вернуть указатель?

StatusCode FooBar(int a, int b, int c, char** output);

Ну, вы поняли.

На стороне вызывающей функциинаиболее часто встречающийся шаблон - использование оператора goto, который указывает на метку очистки:

    if (statusCode < 0) goto error;

    /* snip */
    return everythingWentWell;

error:
    cleanupResources();
    return somethingWentWrong;
2 голосов
/ 15 апреля 2011

Оператор goto - это самый простой и потенциально чистый способ реализации обработки стилей исключений. Использование макроса облегчает чтение, если вы включаете логику сравнения в аргументы макроса. Если вы организуете подпрограммы для выполнения нормальной (то есть не связанной с ошибками) работы и используете goto только для исключений, она будет достаточно чистой для чтения. Например:

/* Exception macro */
#define TRY_EXIT(Cmd)   { if (!(Cmd)) {goto EXIT;} }

/* My memory allocator */
char * MyAlloc(int bytes)
{
    char * pMem = NULL;

    /* Must have a size */
    TRY_EXIT( bytes > 0 );

    /* Allocation must succeed */
    pMem = (char *)malloc(bytes);
    TRY_EXIT( pMem != NULL );

    /* Initialize memory */
    TRY_EXIT( initializeMem(pMem, bytes) != -1 );

    /* Success */
    return (pMem);

EXIT:

    /* Exception: Cleanup and fail */
    if (pMem != NULL)
        free(pMem);

    return (NULL);
}
2 голосов
/ 15 апреля 2011

Как насчет этого?

int NS_Expression(void)
{
    int ok = 1;
    ok = ok && NS_Term();
    ok = ok && Emit("MOVE D0, D1\n");
    ok = ok && NS_AddSub();
    return ok
}
1 голос
/ 16 апреля 2011

Мне никогда не приходило в голову использовать goto или do { } while(0) для обработки ошибок таким образом - это довольно изящно, однако, подумав об этом, я понял, что во многих случаях я могу сделать то же самое, разделив функциюна две:

int Foo(void)
{
    // Initialise things that may need to be cleaned up here.
    char* somePtr = malloc(1024);
    if (somePtr = NULL)
    {
        return NULL;
    }

    if (FooInner(somePtr) < 0)
    {
        // Something went wrong so clean-up.
        free(somePtr);
        return NULL;
    }

    return somePtr;
}

int FooInner(char* somePtr)
{
    if (something(somePtr) < 0) return -1;
    if (somethingElse(somePtr) < 0) return -1;
    // etc

    // if you get here everything's ok.
    return 0;
}

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

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

По крайней мере, обнадеживает то, что я не просто что-то упускаю - у всех остальных есть этопроблема тоже!: -)

0 голосов
/ 19 апреля 2011

Использовать setjmp .

http://en.wikipedia.org/wiki/Setjmp.h

http://aszt.inf.elte.hu/~gsd/halado_cpp/ch02s03.html

http://www.di.unipi.it/~nids/docs/longjump_try_trow_catch.html

#include <setjmp.h>
#include <stdio.h>

jmp_buf x;

void f()
{
    longjmp(x,5); // throw 5;
}

int main()
{
    // output of this program is 5.

    int i = 0;

    if ( (i = setjmp(x)) == 0 )// try{
    {
        f();
    } // } --> end of try{
    else // catch(i){
    {
        switch( i )
        {
        case  1:
        case  2:
        default: fprintf( stdout, "error code = %d\n", i); break;
        }
    } // } --> end of catch(i){
    return 0;
}

#include <stdio.h>
#include <setjmp.h>

#define TRY do{ jmp_buf ex_buf__; if( !setjmp(ex_buf__) ){
#define CATCH } else {
#define ETRY } }while(0)
#define THROW longjmp(ex_buf__, 1)

int
main(int argc, char** argv)
{
   TRY
   {
      printf("In Try Statement\n");
      THROW;
      printf("I do not appear\n");
   }
   CATCH
   {
      printf("Got Exception!\n");
   }
   ETRY;

   return 0;
}
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...