Самый желательный способ обработки сообщений об ошибках функций?

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

Например, скажем, у меня есть функция, которая читает двухбайтовую строку и возвращает ее:

#include <stdio.h>
#include <stdlib.h>

char *bar(void)
{
    char *foo = malloc(3);
    scanf("%2s", foo);
    return foo;
}

int main(void)
{
    char *foo = bar();
    puts(foo);
    free(foo);
    return 0;
}

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

Способ 1 (распечатать сообщение об ошибке в stderr из функции):

#include <stdio.h>
#include <stdlib.h>

char *bar(void)
{
    char *foo;
    if(!(foo = malloc(3)))
    {
        fputs("\nError! Memory allocation failed.", stderr);
        return 0x00;
    }
    scanf("%2s", foo);
    return foo;
}

int main(void)
{
    char *foo;
    if(!(foo = bar())) return 1;
    puts(foo);
    free(foo);
    return 0;
}

Способ 2 (распечатать сообщение об ошибке в stderr из вызывающей функции):

#include <stdio.h>
#include <stdlib.h>

char *bar(void)
{
    char *foo;
    if(!(foo = malloc(3))) return 0x00;
    scanf("%2s", foo);
    return foo;
}

int main(void)
{
    char *foo;
    if(!(foo = bar()))
    {
        fputs("\nError! Memory allocation failed.", stderr); 
        return 1;
    }
    puts(foo);
    free(foo);
    return 0;
}

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

Псевдокод:

 IF FAILUREA
     PRINT "FAILUREA OCCURED"
     RETURN
 IF FAILUREB
     PRINT  "FAILUREB OCCURED"
     RETURN

Это не было бы большой проблемой, если бы функция, которую я вызывал, была int, потому что тогда я мог бы просто вернуть другое целочисленное значение в зависимости от того, что пошло не так. Но в случае char* я обычно пытаюсь вернуть NULL в случае неудачи (поэтому и FAILUREA, и FAILUREB будут возвращать NULL); не было бы никакого способа узнать, что вызвало сбой функции.

Итак, мой вопрос: как лучше всего обрабатывать сообщения об ошибках?


person Keith Miller    schedule 10.10.2012    source источник
comment
Нет, в функции bar(), прямо в методе Method2 Plain с утечкой malloc().   -  person wildplasser    schedule 10.10.2012
comment
Во-первых, ваш malloc нигде не назначает эту память... так что это утечка, несмотря ни на что... но обработка ошибок здесь довольно субъективна. Это действительно зависит от вас, где вы хотите обрабатывать ошибки (или, если вы программируете в ядре Linux, им нравится просто дать ему сбой, чтобы получить обратную трассировку!) Лично я предпочитаю позаботиться об этом в функции, в которой происходит ошибка. держать мой main() в чистоте, но это действительно зависит от вас.   -  person Mike    schedule 10.10.2012
comment
@wildplasser о да, просто написал это на лету, я исправлю это сейчас, и спасибо за вклад, Майк, я ценю это.   -  person Keith Miller    schedule 10.10.2012


Ответы (3)


Разрешить вызывающей стороне обрабатывать отчеты об ошибках лучше, потому что:

  • если функция является частью библиотеки, stderr может быть недоступна, и требуется альтернативный механизм отчетности.
  • вызывающий код может иметь альтернативное действие, которое может быть выполнено, и может не считать сбой функции bar() фактическим сбоем, и ему не нужно сообщать об этом.

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

enum Status
{
    STATUS_OK,
    STATUS_MEMORY_ALLOCATION_FAILURE,
    STATUS_ACCESS_DENIED
};

enum Status status;
char* foo = bar(&status);
if (!foo)
{
    if (STATUS_MEMORY_ALLOCATION_FAILURE == status)
    {
        /* report failure. */
    }
    else if (STATUS_ACCESS_DENIED == status)
    {
        /* try somewhere else */
    }
}
person hmjd    schedule 10.10.2012
comment
Потрясающе, именно то, что я искал. Тоже очень просто и по делу. Спасибо за ваш ответ! - person Keith Miller; 10.10.2012

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

void error(const char* format, ...)
{
  va_list vl;
  va_start(vl, format);
  vfprintf(stderr, format, vl);
  va_end(vl);
  exit(-1);
}

При желании вы можете обернуть его в макрос, указав номер строки и имя файла:

#define ERROR(fmt, ...) \
  error("file:'%s',line:%d " fmt, __FILE__, __LINE__, __VA_ARGS__)

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

Типичное использование, ничего особенного:

char *bar(void)
{
  char *foo;
  if ((foo=malloc(3)) == NULL)
    ERROR("malloc() failed!\n");
  if (scanf("%2s", foo) != 1)
    ERROR("scanf() failed!\n");
  return foo;
}

Вы можете использовать longjmp() вместо exit(-1), чтобы немедленно вернуться к вызывающей стороне (= тому, кто выполнил соответствующее setjmp()), если вы действительно хотите что-то сделать при ошибке, возможно, закрыть все файлы, открытые для записи, чтобы буферизованные данные не потерянный.

Например, если вы пишете простой компилятор, этого типа error() более чем достаточно для большинства ошибок, внутренних для компилятора, и для проблем в компилируемом исходном коде (например, отсутствие двоеточия/паренла или что-то еще, что делает код не компилируется).

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

person Alexey Frunze    schedule 10.10.2012

Вы можете сделать это, если ваша функция возвращает более 1 случая ошибки

#include <stdio.h>
#include <stdlib.h>

int bar(char **foo)
{
    if(!(malloc(3))) return 1; /* return error case 1*/
    scanf("%2s", *foo);
    if(!(malloc(4))) return 2; /* return error case 2*/
    return 0; /* no error*/
}

int catcherror(int error)
{
    switch (error) {
         case 1: 
             /*do something 1*/
         case 2: 
             /*do something 1*/
         case 3: 
             /*do something 1*/
         case 4: 
             /*do something 1*/
         case 5: 
             /*do something 1*/
         default: 
             /*do something 1*/
     }
}

int main(void)
{
    char *foo;
    int error

    error = bar(&foo);
    catcherror(error);
    puts(foo);
    free(foo);
    return 0;
}

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

person MOHAMED    schedule 10.10.2012