Каковы лучшие практики для возврата одной из множества ошибок из функций и завершения программы на C? - PullRequest
0 голосов
/ 20 декабря 2018

Я не могу найти способ вернуть ошибку из функции внутри функции и завершить программу.

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

Допустим, у меня есть такая программа:

int main()
{
  // do stuff 
  importantFunction();
  // do stuff
  return 0;
}

В важный функции () я вызываю две другие функции, которыеделает некоторые Bitshifting и возвращают массив всегда.Теперь я хочу попробовать вернуть 1 (или 0x01, потому что функция возвращает указатель массива), если в одной из этих функций произошла ошибка, но я не уверен, как.

char *importantFunction()
{
  //do stuff
  secoundFunction();
  thirdFunction();
  //do stuff
  return array;
 }


char *secoundFunction()
{
  // do stuff
  if (something == x)
    return array;
  // do stuff
  return array;
}

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

int main()
{
  // do stuff 
  char *pointer = importantFunction();
  if (*pointer == 'something')
    return 1;
  if (*pointer == 'something')
    return 2;
  if (*pointer == 'something')
    return 3;
  // and so on...
  // do stuff
  return 0;
}

Извините, если это глупый вопрос, я не очень хорошо задаю вопросы.

Ответы [ 5 ]

0 голосов
/ 23 декабря 2018

C имеет внеполосную обработку кода ошибки, он был там всегда.

#include <errno.h>

int do_something(char* data) {
   if ( data == 0 ) {
      errno = ENODATA;
      return 0;
   }
   ... do stuff ...
}

... в вызывающей стороне ...

int value = do_something( "one" );
if ( int errornum = errno ) {
   fprintf("error (%d) could not do something: %s", strerror( errornum ) );
   return; // or exit;
} 

если хотитецепочка ошибок

int value = do_something( "one" );
if ( int errornum = errno ) {
   fprintf("error (%d) could not do something: %s", strerror( errornum ) );
   errno = errornum;
   return; // or exit;
} 

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

Причина, по которой errno обычно используется не так интенсивно, как следовало бы, вероятно, заключается в том, что слишком много людей сначала учат внутриполосным сообщениям об ошибках (через специальных дозорных / значений).Кроме того, для правильной проверки кодов ошибок требуется больше строк кода.Тем не менее, это гораздо лучшее решение, так как вы не перегружаете возвращаемое значение данными и управляющей информацией в одной переменной.

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

1  EPERM Operation not permitted
2   ENOENT  No such file or directory
3   ESRCH   No such process
4   EINTR   Interrupted system call
5   EIO     I/O error
6   ENXIO   No such device or address
7   E2BIG   Argument list too long
8   ENOEXEC     Exec format error
9   EBADF   Bad file number
10  ECHILD  No child processes
11  EAGAIN  Try again
12  ENOMEM  Out of memory
13  EACCES  Permission denied
14  EFAULT  Bad address
15  ENOTBLK     Block device required
16  EBUSY   Device or resource busy
17  EEXIST  File exists
18  EXDEV   Cross-device link
19  ENODEV  No such device
20  ENOTDIR     Not a directory
21  EISDIR  Is a directory
22  EINVAL  Invalid argument
23  ENFILE  File table overflow
24  EMFILE  Too many open files
25  ENOTTY  Not a typewriter
26  ETXTBSY     Text file busy
27  EFBIG   File too large
28  ENOSPC  No space left on device
29  ESPIPE  Illegal seek
30  EROFS   Read-only file system
31  EMLINK  Too many links
32  EPIPE   Broken pipe
33  EDOM    Math argument out of domain of func
34  ERANGE  Math result not representable
35  EDEADLK     Resource deadlock would occur
36  ENAMETOOLONG    File name too long
37  ENOLCK  No record locks available
38  ENOSYS  Function not implemented
39  ENOTEMPTY   Directory not empty
40  ELOOP   Too many symbolic links encountered
42  ENOMSG  No message of desired type
43  EIDRM   Identifier removed
44  ECHRNG  Channel number out of range
45  EL2NSYNC    Level 2 not synchronized
46  EL3HLT  Level 3 halted
47  EL3RST  Level 3 reset
48  ELNRNG  Link number out of range
49  EUNATCH     Protocol driver not attached
50  ENOCSI  No CSI structure available
51  EL2HLT  Level 2 halted
52  EBADE   Invalid exchange
53  EBADR   Invalid request descriptor
54  EXFULL  Exchange full
55  ENOANO  No anode
56  EBADRQC     Invalid request code
57  EBADSLT     Invalid slot
59  EBFONT  Bad font file format
60  ENOSTR  Device not a stream
61  ENODATA     No data available
62  ETIME   Timer expired
63  ENOSR   Out of streams resources
64  ENONET  Machine is not on the network
65  ENOPKG  Package not installed
66  EREMOTE     Object is remote
67  ENOLINK     Link has been severed
68  EADV    Advertise error
69  ESRMNT  Srmount error
70  ECOMM   Communication error on send
71  EPROTO  Protocol error
72  EMULTIHOP   Multihop attempted
73  EDOTDOT     RFS specific error
74  EBADMSG     Not a data message
75  EOVERFLOW   Value too large for defined data type
76  ENOTUNIQ    Name not unique on network
77  EBADFD  File descriptor in bad state
78  EREMCHG     Remote address changed
79  ELIBACC     Can not access a needed shared library
80  ELIBBAD     Accessing a corrupted shared library
81  ELIBSCN     .lib section in a.out corrupted
82  ELIBMAX     Attempting to link in too many shared libraries
83  ELIBEXEC    Cannot exec a shared library directly
84  EILSEQ  Illegal byte sequence
85  ERESTART    Interrupted system call should be restarted
86  ESTRPIPE    Streams pipe error
87  EUSERS  Too many users
88  ENOTSOCK    Socket operation on non-socket
89  EDESTADDRREQ    Destination address required
90  EMSGSIZE    Message too long
91  EPROTOTYPE  Protocol wrong type for socket
92  ENOPROTOOPT     Protocol not available
93  EPROTONOSUPPORT     Protocol not supported
94  ESOCKTNOSUPPORT     Socket type not supported
95  EOPNOTSUPP  Operation not supported on transport endpoint
96  EPFNOSUPPORT    Protocol family not supported
97  EAFNOSUPPORT    Address family not supported by protocol
98  EADDRINUSE  Address already in use
99  EADDRNOTAVAIL   Cannot assign requested address
100     ENETDOWN    Network is down
101     ENETUNREACH     Network is unreachable
102     ENETRESET   Network dropped connection because of reset
103     ECONNABORTED    Software caused connection abort
104     ECONNRESET  Connection reset by peer
105     ENOBUFS     No buffer space available
106     EISCONN     Transport endpoint is already connected
107     ENOTCONN    Transport endpoint is not connected
108     ESHUTDOWN   Cannot send after transport endpoint shutdown
109     ETOOMANYREFS    Too many references: cannot splice
110     ETIMEDOUT   Connection timed out
111     ECONNREFUSED    Connection refused
112     EHOSTDOWN   Host is down
113     EHOSTUNREACH    No route to host
114     EALREADY    Operation already in progress
115     EINPROGRESS     Operation now in progress
116     ESTALE  Stale NFS file handle
117     EUCLEAN     Structure needs cleaning
118     ENOTNAM     Not a XENIX named type file
119     ENAVAIL     No XENIX semaphores available
120     EISNAM  Is a named type file
121     EREMOTEIO   Remote I/O error
122     EDQUOT  Quota exceeded
123     ENOMEDIUM   No medium found
124     EMEDIUMTYPE     Wrong medium type
125     ECANCELED   Operation Canceled
126     ENOKEY  Required key not available
127     EKEYEXPIRED     Key has expired
128     EKEYREVOKED     Key has been revoked
129     EKEYREJECTED    Key was rejected by service
130     EOWNERDEAD  Owner died
131     ENOTRECOVERABLE     State not recoverable
0 голосов
/ 20 декабря 2018

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

void error_exit(int code, const char* message) {
  printf("Error %d: %s\nExiting!\n", code, message);
  cleanup();
  exit(code);
}

float* nested_function(int input, ...) {
  if (causes_hopeless_failure(input)) {
    error_exit(err_HOPELESS, "invalid input to nested_function");
  }
  //normal processing proceeds ...
  return valid_pointer;
}

int main() {
   float* vector = function_which_eventually_calls_nested_function();
   cleanup();
   return 0;
}

Функция cleanup используется для работы с ресурсами, которые не очищаются корректно при выходе из программы,Дескрипторы файлов и выделенные буферы обычно не попадают в эту категорию.Обычно я использую его для изменений конфигурации системы, которые необходимо отменить.

0 голосов
/ 20 декабря 2018

Не возвращайте внеполосные коды ошибок с указателями.Указатель имеет значение NULL или допустимо (если он недействителен, как правило, стек переполняется в вашей программе).Выполнение char *pointer = (char*)(uintptr_t)1; просто сбивает с толку, а выполнение if ((uintptr_t)pointer == 1) {.. } столь же необоснованно.

Возвращает int.int обычно в стандартной библиотеке C возвращает ошибку.Обычно библиотеки C возвращают -1 при ошибке и устанавливают errno - я обычно пишу код библиотеки, который возвращает отрицательное значение, которое является кодом ошибки (т. Е. return -ENOMEM в случае сбоя malloc).Возвращайте 0 в случае успеха и, возможно, положительные значения, чтобы уведомить коды пользователей о некотором «состоянии» в библиотеке.Передайте все переменные, которые вы хотите установить, указателем.Посмотрите на бывших fopen_s (мнение: не используйте fopen_s, только посмотрите на него).

enum importantFunction_rets_e {
   IMPORTANT_FUNCTION_ERR_1 = -1,
   IMPORTANT_FUNCTION_ERR_2 = -2,
   IMPORTANT_FUNCTION_STATE_1 = 1,
   IMPORTANT_FUNCTION_STATE_2 = 2,
};

int importantFunction(char **pointer)
{
   assert(pointer != NULL);
   // or maybe
   if (pointer == NULL) return -EINVAL;

   int ret;
   ret = secondFunction(pointer);
   if (ret < 0) return ret;
   ret = thirdFunction(pointer);
   if (ret < 0) return ret; 
   return 0;
}

int secondFunction(char **pointer) {
  *pointer = malloc(sizeof(char) * 5);
   if (*pointer == NULL) {
      return IMPORTANT_FUNCTION_ERR_1;
   }
   memcpy(*pointer, "hey!", 5);
   return 0;
}

int main() {
    char *pointer;
    const int importantFunction_ret = importantFunction(&pointer);
    if (importantFunction_ret < 0) {
       if (importantFunction_ret == IMPORTANT_FUNCTION_ERR_1) {
          // handle err 1
       } else if (importantFunction_ret == IMPORTANT_FUNCTION_ERR_2) {
          // handle err 2
       } else {
          // hanlde other errors
       }
       return -1;
    }
    if (importantFunction_ret == IMPORTANT_FUNCTION_STATE_1) { 
         // handle state1
    } else if {importantFunction_ret == IMPORTANT_FUNCTION_STATE_2) {
        // handle state2
    } else {
        // handle other states 
        assert(0);
    }
}

Если вы хотите изучить тему или обработку ошибок в C, вы можете реализовать что-товместе с новым (или старым?) предложением , в котором используется тот же метод, что и в объектно-ориентированных языках, std::variant или std::expected или аналогичным (мнение: я действительно против этого предложения, как сейчас, он нуждается в редизайне / рефакторинге, но это будет БОЛЬШОЙ шаг вперед для C).

0 голосов
/ 20 декабря 2018

Существует три распространенных способа возврата ошибки из функции:

  1. Пусть функция возвращает int, с конкретными значениями, указывающими на успех и неудачу

    Например, возврат EXIT_SUCCESS или EXIT_FAILURE из main() - это способ, которым стандарты C рекомендуют сообщать об успехе или неудаче всего процесса.(Варианты BSD пытались стандартизировать некоторые другие коды; если ваша система имеет заголовок <sysexits.h>, вы можете использовать их. Но учтите, что они не являются «стандартными», просто мы ближе всего согласны с тем, как процесс можетсообщить коды ошибок.)

  2. Зарезервируйте конкретное возвращаемое значение для ошибок и используйте глобальную или локальную переменную потока (обычно errno) для описания ошибки

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

  3. Используйте дополнительный параметр, чтобы указать на индикатор ошибки.

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

Мои собственные правила просты:

  • Предпочитайте второй подход при написании низкоуровневой библиотеки.Это знакомый подход для тех, кто знаком со стандартной библиотекой C.

  • Используйте первый подход для исправляемых ошибок.

    Часто я комбинирую его со вторым, используяreturn 0; для успеха и return errno; или return errno = EINVAL; и т. Д. Для ошибок.(Последний сначала назначает EINVAL для errno, а затем возвращает EINVAL.)

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


Давайте посмотрим, как эти методы отличаются на практике.

Очень распространенная вещь, которую нужно сделать, - это анализ командыаргументы в виде чисел.Давайте рассмотрим случай, когда аргументы должны использоваться как double s, для какого-то рода вычислений:

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

int main(int argc, char *argv[])
{
    int     arg;
    double  val;    

    for (arg = 1; arg < argc; arg++) {
        if (sscanf(argv[arg], "%lf", &val) == 1) {
            printf("argv[%d] = %.6f\n", arg, val);
        } else {
            printf("%s: Not a number.\n", argv[arg]);
            exit(EXIT_FAILURE);
        }
    }

    return EXIT_SUCCESS;
}

Выше приведено sscanf() для преобразования строки.К сожалению, он не проверяет наличие конечного мусора, поэтому он принимает, например, 1.5k как 1.5.Чтобы избежать этого, мы можем использовать фиктивный символ для обнаружения завершающего мусора:

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

int main(int argc, char *argv[])
{
    int     arg;
    double  val;
    char    dummy;

    for (arg = 1; arg < argc; arg++) {
        if (sscanf(argv[arg], "%lf %c", &val, &dummy) == 1) {
            printf("argv[%d] = %.6f\n", arg, val);
        } else {
            printf("%s: Not a number.\n", argv[arg]);
            exit(EXIT_FAILURE);
        }
    }

    return EXIT_SUCCESS;
}

Это работает, потому что sscanf() возвращает количество успешных преобразований, и мы ожидаем только двойное преобразование (%lf)к работе, и преобразование символов (%c) завершится неудачей.

К сожалению, семейство функций scanf не проверяет переполнение.Если вы поставите достаточно большое количество, оно будет молча повреждено.Нехорошо.Чтобы избежать этого, мы можем использовать strtod().Чтобы упростить использование, мы можем поместить его в отдельную функцию parse_double().Но как это должно вернуть значение и возможную ошибку?Что из нижеперечисленного реализовать?

/* Convert the initial double, returning the pointer to the rest of the
   string; or NULL if an error occurs. */
const char *parse_double(const char *src, double *to);

/* If the string contains exactly one double, convert it and return 0.
   Otherwise return a nonzero error code. */
int parse_double(const char *src, double *to);

/* Convert the string to a double as best as you can. If an error occurs, return 'errval'. */
double parse_double(const char *src, const double errval);

Итак, какой из них лучший?

Ответ, конечно, зависит от варианта использования .

Я фактически реализовал все три (в отдельных программах), в зависимости от того, какая из них была наиболее подходящей.

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

Второй - это то, что я чаще всего использую в программах.Очень часто я использую

typedef struct {
    double  x;
    double  y;
    double  z;
} vec3d;

int parse_vector(const char *src, vec3d *to)
{
    vec3d  temp;
    char   dummy;

    if (!src || !*src)
        return -1; /* NULL or empty string */

    if (sscanf(src, " %lf %lf %lf %c", &temp.x, &temp.y, &temp.z, &dummy) == 3 ||
        sscanf(src, " %lf %*[.,:/] %lf %*[.,:/] %lf %c", &temp.x, &temp.y, &temp.z, &dummy) == 3) {
        if (to)
            *to = temp;
        return 0;
    }

    return -1;
}

, что позволяет указывать трехмерный вектор в командной строке, используя 1+2+3, 1/2/3, 1:2:3 или даже '1 2 3' или "1 2 3" (кавычки необходимы, чтобы помешать оболочке разделить ее на три отдельных аргумента).Он не проверяет переполнение double, поэтому важно показать проанализированный вектор в выходных данных, чтобы пользователь мог определить, был ли их ввод неверным.

(Звездочка * в %*[.,:/] означает, что результат преобразования нигде не хранится, и преобразование не учитывается в возвращаемом значении. [ является спецификатором преобразования, "преобразующим" все и все изсимволы в списке, оканчивающиеся символом ]. [^ - это инверсия, которая "преобразует" все символы , а не в списке.)

0 голосов
/ 20 декабря 2018

Распространенный способ обработки ошибок в Си - через возвращаемые значения.

Скажем, функция f в случае успеха возвращает указатель на строку символов.

char *f();

ТакаяВ случае сбоя функции возвращали бы указатель NULL, который можно получить, включая некоторые общие заголовочные файлы (например, <string.h>).

Теперь скажем, g - это функция, которая имеет целое число, вычисляетчто-то и возвращает целочисленный результат операции, но функция может завершиться ошибкой (например, параметр недопустим для вычисления, кто знает ...).Тогда, возможно, вы захотите написать это так:

int g(int i, int *result);

Здесь i - параметр для вычисления чего-либо, а result - указатель на переменную, в которой вы будете хранить результат. Теперьпочему g возвращает тип int?Ну, это может быть bool из <stdbool.h>, но обычно используется int ... Возвращаемое значение будет использоваться как логическое значение, g вернет 0 в случае сбоя и 1 в случае успеха.

Вы можете использовать их в третьей функции h

int h(int i) {
    char *str = f();

    if (str == NULL) {
        printf("f failed !\n");
        return 0; // f failed
    }

    printf("%s\n", str);

    int result;
    if (!g(i, &result)) {
        printf("g failed !\n");
        return 0; // g failed
    } else {
        printf("result = %d\n", result);
    }

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