C 코드에서 오류 처리


152

C 라이브러리에서 일관된 방식으로 오류 처리 오류와 관련하여 "모범 사례"는 무엇이라고 생각하십니까?

내가 생각한 두 가지 방법이 있습니다.

항상 오류 코드를 반환하십시오. 일반적인 기능은 다음과 같습니다.

MYAPI_ERROR getObjectSize(MYAPIHandle h, int* returnedSize);

항상 오류 포인터 접근 방식을 제공합니다.

int getObjectSize(MYAPIHandle h, MYAPI_ERROR* returnedError);

첫 번째 접근 방식을 사용할 때 오류 처리 검사가 함수 호출에 직접 배치되는 다음과 같은 코드를 작성할 수 있습니다.

int size;
if(getObjectSize(h, &size) != MYAPI_SUCCESS) {
  // Error handling
}

오류 처리 코드보다 나은 것 같습니다.

MYAPIError error;
int size;
size = getObjectSize(h, &error);
if(error != MYAPI_SUCCESS) {
    // Error handling
}

그러나 데이터를 반환하기 위해 반환 값을 사용하면 코드를 더 쉽게 읽을 수 있다고 생각합니다. 두 번째 예제에서 size 변수에 무언가가 쓰여진 것은 분명합니다.

내가 왜 이러한 접근 방식을 선호하거나 혼합하거나 다른 것을 사용해야하는지에 대한 아이디어가 있습니까? 라이브러리의 멀티 스레드 사용을보다 고통스럽게 만드는 경향이 있기 때문에 전역 오류 상태에 관심이 없습니다.

편집 : 이것에 대한 C ++ 관련 아이디어는 예외가 아니기 때문에 현재로서는 나에게 옵션이 아니기 때문에 들리는 것이 흥미로울 것입니다 ...


나는 약 2 주 동안 C를 배웠지 만 OUT 매개 변수는 값별로 구조체를 반환하는 오버 헤드를 피하고 완화하기 때문에 OUT 매개 변수는 대부분의 함수에 대한 사실상 반환 값이라는 것입니다. 대부분의 변수가 스택에 있으므로 메모리 할당을 해제해야합니다. 따라서 함수의 실제 값으로 "return"을 사용하지 않기 때문에 대부분의 경우 오류 처리에 자유롭게 사용할 수 있습니다.
Joel Roberts

답변:


74

오류를 반환 값 방식으로 좋아합니다. API를 디자인하고 라이브러리를 가능한 한 고통없이 사용하려면 다음 추가 사항에 대해 생각하십시오.

  • 가능한 모든 오류 상태를 하나의 typedef 열거 형 열거 형에 저장하고 lib에서 사용하십시오. 정수를 반환하거나 더 나쁜 결과를 반환하지 말고 정수 또는 다른 열거 형을 리턴 코드와 혼합하십시오.

  • 오류를 사람이 읽을 수있는 것으로 변환하는 기능을 제공합니다. 간단 할 수 있습니다. const-char * out 오류가 발생했습니다.

  • 이 아이디어는 멀티 스레드 사용을 약간 어렵게하지만 응용 프로그램 프로그래머가 전역 오류 콜백을 설정할 수 있다면 좋을 것입니다. 그렇게하면 버그 헌트 세션 중에 콜백에 중단 점을 넣을 수 있습니다.

도움이 되길 바랍니다.


5
"이 아이디어로 인해 멀티 스레드 사용이 약간 어려워집니다." 멀티 스레딩으로 인해 어느 부분이 어려워 집니까? 간단한 예를 들어 줄 수 있습니까?
SayeedHussain

1
@crypticcoder 간단히 말해서 : 전역 오류 콜백은 모든 스레드 컨텍스트에서 호출 할 수 있습니다. 오류를 인쇄하면 아무런 문제가 없습니다. 문제를 해결하려고하면 어떤 호출 스레드가 오류를 일으켰는지 알아 내야하므로 문제가 발생합니다.
Nils Pipenbrinck 2014 년

9
오류에 대한 자세한 내용을 전달하려면 어떻게합니까? 예를 들어 파서 오류가 있고 구문 오류의 줄 번호와 열 및 모두를 멋지게 인쇄하는 방법을 제공하려고합니다.
panzi

1
@panzi 그런 다음 분명히 구조체를 반환하거나 구조체가 실제로 큰 경우 out 포인터를 사용하고 구조체를 문자열로 형식화하는 기능이 있어야합니다.
Winger Sendon

여기 코드의 첫 번째 2 개 총알을 보여줍니다 stackoverflow.com/questions/385975/error-handling-in-c-code/...
가브리엘 스테이 플스

92

나는 두 가지 접근법을 모두 사용했으며 둘 다 잘 작동했습니다. 어느 것을 사용하든 항상이 원칙을 적용하려고합니다.

가능한 오류가 프로그래머 오류 인 경우 오류 코드를 반환하지 말고 함수 내에 assert를 사용하십시오.

입력을 검증하는 어설 션은 함수가 기대하는 것을 명확하게 전달하지만 너무 많은 오류 검사는 프로그램 논리를 모호하게 할 수 있습니다. 모든 다양한 오류 사례에 대해 수행 할 작업을 결정하면 실제로 설계가 복잡해질 수 있습니다. 프로그래머가 절대로 전달하지 않도록 주장 할 수 있다면 functionX가 널 포인터를 처리하는 방법을 알아내는 이유는 무엇입니까?


1
C에서 주장하는 예가 있습니까? (나는 C에게 매우 녹색이다)
thomthom

일반적으로 assert(X)X가 유효한 C 문인 것처럼 간단 합니다. stackoverflow.com/q/1571340/10396을 참조하십시오 .
AShelly

14
어쨌든 절대 절대 라이브러리 코드에서 어설 션을 사용하지 마십시오 ! 또한, 다른 코드 와 같이 하나의 코드에서 다양한 스타일의 오류 처리를 혼합하지 마십시오 .
mirabilos

10
나는 스타일을 섞지 않는 것에 동의합니다. 당신의 주장에 대한 당신의 추론이 궁금합니다. 함수 문서에 "argument X는 NULL이 아니어야합니다"또는 "Y는이 열거 형의 멤버 여야합니다"라고 표시되어있는 경우 assert(X!=NULL);또는 assert(Y<enumtype_MAX);? 이것이 올바른 방법이라고 생각하는 이유에 대한 자세한 내용 은 프로그래머 와이 질문에 대한 질문에 대한이 답변을 참조하십시오 .
AShelly

8
@AShelly 문제는 일반적으로 릴리스 빌드에 없다고 주장합니다.
Calmarius

29

CMU의 CERT에는 일반적인 C (및 C ++) 오류 처리 기술 각각을 사용할시기에 대한 권장 사항 이 포함 된 멋진 슬라이드 세트가 있습니다 . 가장 좋은 슬라이드 중 하나는이 의사 결정 트리입니다.

오류 처리 의사 결정 트리

이 flowcart에 대해 개인적으로 두 가지를 변경하려고합니다.

먼저, 때로는 객체가 오류를 나타 내기 위해 반환 값을 사용해야한다는 것을 분명히하고 싶습니다. 함수가 객체에서 데이터를 추출하지만 객체를 변경하지 않으면 객체 자체의 무결성이 위험하지 않으며 반환 값을 사용하여 오류를 나타내는 것이 더 적합합니다.

둘째, C ++에서 예외를 사용하는 것이 항상 적절한 것은 아닙니다 . 예외 처리는 오류 처리에 전념하는 소스 코드의 양을 줄일 수 있고 대부분 함수 시그니처에 영향을 미치지 않으며 콜 스택을 전달할 수있는 데이터가 매우 유연하기 때문에 좋습니다. 반면에 몇 가지 이유로 예외가 올바른 선택이 아닐 수 있습니다.

  1. C ++ 예외에는 매우 특정한 의미가 있습니다. 이러한 의미를 원하지 않으면 C ++ 예외가 나쁜 선택입니다. 예외가 발생하면 즉시 처리해야하며, 설계는 오류가 콜 스택을 몇 단계 풀어야하는 경우를 선호합니다.

  2. 예외를 던지는 C ++ 함수는 예외를 처리하기 위해 랩핑 할 수 없으며, 적어도 예외의 전체 비용을 지불하지 않으면 예외는 발생하지 않습니다. 오류 코드를 반환하는 함수는 C ++ 예외를 발생시키기 위해 랩핑되어보다 유연합니다. C ++ new은 던지지 않는 변형을 제공하여이 권리를 얻습니다.

  3. C ++ 예외는 상대적으로 비싸지 만이 단점은 예외를 현명하게 사용하는 프로그램에 비해 과장된 것입니다. 프로그램은 단순히 성능이 중요한 코드 경로에서 예외를 발생시키지 않아야합니다. 프로그램이 얼마나 빨리 오류를보고하고 종료 할 수 있는지는 중요하지 않습니다.

  4. 때로는 C ++ 예외를 사용할 수 없습니다. C ++ 구현에서 문자 그대로 사용할 수 없거나 코드 지침에서이를 금지합니다.


원래의 질문은 멀티 스레드 컨텍스트에 관한 것이기 때문에 로컬 오류 표시기 기술 ( SirDarius답변에 설명되어 있음 )이 원래 답변에서 과소 평가 되었다고 생각합니다 . 스레드 안전하고 호출자가 즉시 오류를 처리하지 않으며 오류를 설명하는 임의의 데이터를 묶을 수 있습니다. 단점은 객체에 의해 유지되어야하며 (어떻게 든 외부와 관련이 있다고 가정) 반환 코드보다 무시하기 쉽다는 것입니다.


5
Google의 C ++ 코딩 표준은 여전히 C ++ 예외를 사용하지 않는다고
Jonathan Leffler 2016

19

라이브러리를 만들 때마다 첫 번째 방법을 사용합니다. typedef'ed 열거 형을 리턴 코드로 사용하면 몇 가지 장점이 있습니다.

  • 함수가 배열과 같은 더 복잡한 출력을 반환하고 길이가 길면 임의의 구조를 만들어 반환 할 필요가 없습니다.

    rc = func(..., int **return_array, size_t *array_length);
  • 간단하고 표준화 된 오류 처리가 가능합니다.

    if ((rc = func(...)) != API_SUCCESS) {
       /* Error Handling */
    }
  • 라이브러리 함수에서 간단한 오류 처리가 가능합니다.

    /* Check for valid arguments */
    if (NULL == return_array || NULL == array_length)
        return API_INVALID_ARGS;
  • typedef'ed 열거 형을 사용하면 디버거에서 열거 형 이름을 볼 수도 있습니다. 이를 통해 지속적으로 헤더 파일을 참조 할 필요없이 디버깅이 쉬워집니다. 이 열거 형을 문자열로 변환하는 기능도 도움이됩니다.

사용 된 접근 방식에 관계없이 가장 중요한 문제는 일관된 것입니다. 이는 함수 및 인수 이름 지정, 인수 순서 및 오류 처리에 적용됩니다.


9

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;
}

2
두 번째 코드 블록은 답변 맨 위에서 참조 된 Francesco Nidito 페이지 의 이전 코드 버전을 기반으로합니다 . ETRY이 답변이 작성된 이후 코드가 개정되었습니다.
Jonathan Leffler 2018 년

2
Setjmp는 끔찍한 오류 처리 전략입니다. setjmp와 longjmp 호출 사이에 자원을 할당하면 비용이 많이 들고 오류가 발생하기 쉬우 며 (비 휘발성 변경 로컬에 변경 값이 모두 유지되지 않음) 리소스가 누출됩니다. sigjmp / longjmp 비용을 회수하기 전에 30 회 반품 및 int-val 점검을 수행 할 수 있어야합니다. 대부분의 콜 스택은 특히 재귀에 중점을 두지 않으면 깊이 깊숙이 가지 않습니다 (그렇게하면 반환 + 검사 비용 이외의 성능 문제가 있음).
PSkocik

1
메모리를 malloc 한 다음 던지면 메모리가 영원히 누출됩니다. 또한 setjmp오류가 발생하지 않더라도 꽤 많은 CPU 시간과 스택 공간을 소비합니다. Windows 용 gcc를 사용하는 경우 C ++에 대한 여러 가지 예외 처리 방법 중 하나를 선택할 수 setjmp있으며 실제로는 코드를 최대 30 % 느리게 만들 수 있습니다.
Mecki

7

나는 개인적으로 이전 접근법을 선호합니다 (오류 표시기 반환).

필요한 경우 반환 결과는 오류가 발생했음을 나타내며 정확한 오류를 찾는 데 다른 함수가 사용됩니다.

getSize () 예제에서는 size가 항상 0 또는 양수 여야한다고 생각하므로 UNIX 시스템 호출과 마찬가지로 음수 결과를 반환하면 오류를 나타낼 수 있습니다.

포인터로 전달 된 오류 객체와 함께 후자의 접근법에 사용되는 라이브러리를 생각할 수 없습니다. stdio등은 모두 반환 값으로 이동합니다.


1
레코드의 경우, 후자의 접근 방식을 사용한 라이브러리 중 하나는 Maya 프로그래밍 API입니다. C보다는 C ++ 라이브러리입니다. 오류를 처리하는 방식이 상당히 일치하지 않으며 때로는 오류가 반환 값으로 전달되고 다른 경우에는 결과를 참조로 전달합니다.
Laserallan

1
strtod를 잊지 마십시오. 마지막 주장은 오류를 나타내는 것뿐만 아니라 오류를 나타내는 것입니다.
quinmars

7

프로그램을 작성할 때 초기화 중에 일반적으로 오류 처리를 위해 스레드를 분리하고 잠금을 포함하여 오류에 대한 특수 구조를 초기화합니다. 그런 다음 반환 값을 통해 오류를 감지하면 예외의 정보를 구조에 입력하고 SIGIO를 예외 처리 스레드로 보낸 다음 실행을 계속할 수 없는지 확인합니다. 할 수 없으면 SIGURG를 예외 스레드로 보내 프로그램을 정상적으로 중지합니다.


7

오류 코드를 반환하는 것은 C에서 오류를 처리하는 일반적인 방법입니다.

그러나 최근에 나가는 오류 포인터 접근 방식을 실험했습니다.

반환 값 접근 방식에 비해 몇 가지 장점이 있습니다.

  • 보다 의미있는 목적으로 반환 값을 사용할 수 있습니다.

  • 해당 오류 매개 변수를 작성하면 오류를 처리하거나 전파해야합니다. (의 반환 값을 확인하는 것을 잊지 fclose마십시오.)

  • 오류 포인터를 사용하면 함수를 호출 할 때 아래로 전달할 수 있습니다. 기능 중 하나라도 설정하면 값이 손실되지 않습니다.

  • 오류 변수에 데이터 중단 점을 설정하여 오류가 처음 발생한 위치를 파악할 수 있습니다. 조건부 중단 점을 설정하면 특정 오류도 포착 할 수 있습니다.

  • 모든 오류 처리 여부를보다 쉽게 ​​자동화 할 수 있습니다. 코드 규칙에 따라 오류 포인터를 강제로 호출 할 err수 있으며 마지막 인수 여야합니다. 따라서 스크립트는 문자열 err);을 일치시킬 수 있고 뒤에 문자열이 있는지 확인하십시오 if (*err. 실제로 우리는 CER(err return 검사)와 CEG(err goto 검사 ) 라는 매크로를 만들었습니다 . 따라서 오류가 발생했을 때 항상 입력해야 할 필요가 없으며 시각적 혼란을 줄일 수 있습니다.

우리 코드의 모든 함수가 나가는 매개 변수를 가지고 있지는 않습니다. 이 나가는 매개 변수는 일반적으로 예외를 throw하는 경우에 사용됩니다.


6

나는 과거에 많은 C 프로그래밍을 해왔다. 그리고 실제로 오류 코드 반환 값을 계산했습니다. 그러나 몇 가지 함정이 있습니다.

  • 중복 오류 번호는 전역 errors.h 파일로 해결할 수 있습니다.
  • 오류 코드를 확인하는 것을 잊어 버렸지 만 단서와 긴 디버깅 시간으로 해결해야합니다. 그러나 결국에는 배우게됩니다 (또는 다른 사람이 디버깅을 수행한다는 것을 알게 될 것입니다).

2
두 번째 문제는 적절한 컴파일러 경고 수준, 적절한 코드 검토 메커니즘 및 정적 코드 분석기 도구를 통해 해결할 수 있습니다.
Ilya

1
API 함수가 호출되고 반환 값이 확인되지 않으면 버그가 있습니다.
Jonathan Leffler

6

유닉스 접근법은 두 번째 제안과 가장 비슷합니다. 결과 또는 하나의 "잘못되었습니다"값을 반환하십시오. 예를 들어, open은 성공하면 파일 디스크립터를, 실패하면 -1을 리턴합니다. 또한 실패 시 발생 하는 실패 errno를 나타내는 외부 전역 정수를 설정합니다 .

그 가치에 대해 Cocoa도 비슷한 접근법을 채택하고 있습니다. 많은 메소드가 BOOL을 리턴하고 NSError **매개 변수를 가져 오면 실패시 오류를 설정하고 NO를 리턴합니다. 그런 다음 오류 처리는 다음과 같습니다.

NSError *error = nil;
if ([myThing doThingError: &error] == NO)
{
  // error handling
}

두 옵션 사이에 있습니다 :-).



5

여기에 약간의 훈련이 필요한 재미있는 방법이 있습니다.

이는 핸들 유형 변수가 모든 API 함수를 작동하는 인스턴스라고 가정합니다.

아이디어는 핸들 뒤에있는 구조체가 이전 데이터를 필요한 데이터 (코드, 메시지 ...)가있는 구조체로 저장하고 사용자 에게이 오류 객체에 대한 포인터를 반환하는 함수를 제공한다는 것입니다. 각 작업은 지정된 개체를 업데이트하여 사용자가 함수를 호출하지 않고도 상태를 확인할 수 있습니다. errno 패턴과 달리 오류 코드는 전역 적이 지 않으므로 각 핸들이 올바르게 사용되는 한 접근 방식이 스레드로부터 안전합니다.

예:

MyHandle * h = MyApiCreateHandle();

/* first call checks for pointer nullity, since we cannot retrieve error code
   on a NULL pointer */
if (h == NULL)
     return 0; 

/* from here h is a valid handle */

/* get a pointer to the error struct that will be updated with each call */
MyApiError * err = MyApiGetError(h);


MyApiFileDescriptor * fd = MyApiOpenFile("/path/to/file.ext");

/* we want to know what can go wrong */
if (err->code != MyApi_ERROR_OK) {
    fprintf(stderr, "(%d) %s\n", err->code, err->message);
    MyApiDestroy(h);
    return 0;
}

MyApiRecord record;

/* here the API could refuse to execute the operation if the previous one
   yielded an error, and eventually close the file descriptor itself if
   the error is not recoverable */
MyApiReadFileRecord(h, &record, sizeof(record));

/* we want to know what can go wrong, here using a macro checking for failure */
if (MyApi_FAILED(err)) {
    fprintf(stderr, "(%d) %s\n", err->code, err->message);
    MyApiDestroy(h);
    return 0;
}

4

첫 번째 방법은 IMHO가 더 낫습니다.

  • 그런 식으로 함수를 작성하는 것이 더 쉽습니다. 함수 중간에 오류가 발생하면 오류 값만 반환합니다. 두 번째 접근 방식에서는 매개 변수 중 하나에 오류 값을 할당 한 다음 무언가를 반환해야합니다 .... 무엇을 반환합니까? 올바른 값이 없으며 오류 값을 반환하지 않습니다.
  • 더 대중적이므로 이해하기 쉽고 유지 관리가 쉽습니다.

4

나는 분명히 첫 번째 솔루션을 선호합니다 :

int size;
if(getObjectSize(h, &size) != MYAPI_SUCCESS) {
  // Error handling
}

나는 그것을 약간 수정했다 :

int size;
MYAPIError rc;

rc = getObjectSize(h, &size)
if ( rc != MYAPI_SUCCESS) {
  // Error handling
}

또한 현재 함수 범위를 통해 허용 할지라도 합법적 인 반환 값을 오류와 절대로 혼합하지 않습니다. 함께 함수 구현이 앞으로 어떻게 진행되는지 알 수 없습니다.

우리가 이미 오류 처리에 대해 이야기한다면 goto Error;undo 오류 처리를 올바르게 처리하기 위해 일부 함수를 호출 할 수 없다면 오류 처리 코드로 할 것입니다.


3

오류를 반환하지 않고 함수로 데이터를 반환하지 못하게 할 수있는 방법은 반환 유형에 래퍼 를 사용하는 것입니다 .

typedef struct {
    enum {SUCCESS, ERROR} status;
    union {
        int errCode;
        MyType value;
    } ret;
} MyTypeWrapper;

그런 다음 호출 된 함수에서

MyTypeWrapper MYAPIFunction(MYAPIHandle h) {
    MyTypeWrapper wrapper;
    // [...]
    // If there is an error somewhere:
    wrapper.status = ERROR;
    wrapper.ret.errCode = MY_ERROR_CODE;

    // Everything went well:
    wrapper.status = SUCCESS;
    wrapper.ret.value = myProcessedData;
    return wrapper;
} 

다음 방법을 사용하면 래퍼는 MyType의 크기와 1 바이트 (대부분의 컴파일러에서)의 크기를 가지게되므로 수익성이 높습니다. 함수를 호출 할 때 ( 또는 제시 한 두 가지 방법 모두) 스택에서 다른 인수를 푸시 할 필요가 없습니다 .returnedSizereturnedError


3

다음은 Nils Pipenbrinck의 첫 번째 총알 2 가지를 보여주는 간단한 프로그램입니다. 입니다.

그의 첫 번째 총알은 다음과 같습니다.

  • 가능한 모든 오류 상태를 하나의 typedef 열거 형 열거 형에 저장하고 lib에서 사용하십시오. 정수를 반환하거나 더 나쁜 결과를 반환하지 말고 정수 또는 다른 열거 형을 리턴 코드와 혼합하십시오.

  • 오류를 사람이 읽을 수있는 것으로 변환하는 기능을 제공합니다. 간단 할 수 있습니다. const-char * out 오류가 발생했습니다.

라는 모듈을 작성했다고 가정하십시오 mymodule. 먼저 mymodule.h에서 열거 형 오류 코드를 정의하고 이러한 코드에 해당하는 오류 문자열을 작성합니다. 여기서는 char *첫 번째 열거 기반 오류 코드의 값이 0 인 경우에만 잘 작동하는 C 문자열 배열 ( )을 사용하고 그 이후의 숫자를 조작하지 않습니다. 간격이나 다른 시작 값과 함께 오류 코드 번호를 사용하는 경우 매핑 된 C 문자열 배열을 사용하는 것에서 (아래와 같이) switch 문을 사용하는 함수를 사용하거나 if / else if 문을 변경해야합니다. 열거 형 오류 코드에서 인쇄 가능한 C 문자열 (내가 보여주지 않음)에 매핑합니다. 선택은 당신입니다.

mymodule.h

/// @brief Error codes for library "mymodule"
typedef enum mymodule_error_e
{
    /// No error
    MYMODULE_ERROR_OK = 0,
    
    /// Invalid arguments (ex: NULL pointer where a valid pointer is required)
    MYMODULE_ERROR_INVARG,

    /// Out of memory (RAM)
    MYMODULE_ERROR_NOMEM,

    /// Make up your error codes as you see fit
    MYMODULE_ERROR_MYERROR, 

    // etc etc
    
    /// Total # of errors in this list (NOT AN ACTUAL ERROR CODE);
    /// NOTE: that for this to work, it assumes your first error code is value 0 and you let it naturally 
    /// increment from there, as is done above, without explicitly altering any error values above
    MYMODULE_ERROR_COUNT,
} mymodule_error_t;

// Array of strings to map enum error types to printable strings
// - see important NOTE above!
const char* const MYMODULE_ERROR_STRS[] = 
{
    "MYMODULE_ERROR_OK",
    "MYMODULE_ERROR_INVARG",
    "MYMODULE_ERROR_NOMEM",
    "MYMODULE_ERROR_MYERROR",
};

// To get a printable error string
const char* mymodule_error_str(mymodule_error_t err);

// Other functions in mymodule
mymodule_error_t mymodule_func1(void);
mymodule_error_t mymodule_func2(void);
mymodule_error_t mymodule_func3(void);

mymodule.c에는 열거 형 오류 코드에서 인쇄 가능한 C 문자열로 매핑하는 매핑 기능이 포함되어 있습니다.

mymodule.c

#include <stdio.h>

/// @brief      Function to get a printable string from an enum error type
/// @param[in]  err     a valid error code for this module
/// @return     A printable C string corresponding to the error code input above, or NULL if an invalid error code
///             was passed in
const char* mymodule_error_str(mymodule_error_t err)
{
    const char* err_str = NULL;

    // Ensure error codes are within the valid array index range
    if (err >= MYMODULE_ERROR_COUNT)
    {
        goto done;
    }

    err_str = MYMODULE_ERROR_STRS[err];

done:
    return err_str;
}

// Let's just make some empty dummy functions to return some errors; fill these in as appropriate for your 
// library module

mymodule_error_t mymodule_func1(void)
{
    return MYMODULE_ERROR_OK;
}

mymodule_error_t mymodule_func2(void)
{
    return MYMODULE_ERROR_INVARG;
}

mymodule_error_t mymodule_func3(void)
{
    return MYMODULE_ERROR_MYERROR;
}

main.c에는 일부 함수 호출 및 오류 코드 인쇄를 보여주는 테스트 프로그램이 포함되어 있습니다.

main.c

#include <stdio.h>

int main()
{
    printf("Demonstration of enum-based error codes in C (or C++)\n");

    printf("err code from mymodule_func1() = %s\n", mymodule_error_str(mymodule_func1()));
    printf("err code from mymodule_func2() = %s\n", mymodule_error_str(mymodule_func2()));
    printf("err code from mymodule_func3() = %s\n", mymodule_error_str(mymodule_func3()));

    return 0;
}

산출:

C (또는 C ++)의 열거 기반 오류 코드 데모
mymodule_func1 () = MYMODULE_ERROR_OK의
오류 코드 mymodule_func2 () = MYMODULE_ERROR_INVARG의
오류 코드 mymodule_func3 ()의 오류 코드 : MYMODULE_ERROR_MYERROR

참고 문헌 :

https://onlinegdb.com/ByEbKLupS 에서이 코드를 직접 실행할 수 있습니다 .


2

말한 것 외에도, 오류 코드를 리턴하기 전에 오류가 리턴 될 때 어설 션 또는 이와 유사한 진단을 실행하면 추적이 훨씬 쉬워집니다. 이 작업을 수행하는 방법은 릴리스시 여전히 컴파일되어 있지만 소프트웨어가 진단 모드 인 경우에만 자동으로 로그 파일에보고하거나 화면에서 일시 중지하는 옵션이있는 사용자 지정 어설 ​​션을 사용하는 것입니다.

개인적으로 오류 코드를 no_error 가 0 인 음의 정수로 반환 하지만 다음과 같은 버그가 생길 수 있습니다

if (MyFunc())
 DoSomething();

대안은 실패가 항상 0으로 리턴되고 LastError () 함수를 사용하여 실제 오류에 대한 세부 사항을 제공하는 것입니다.


2

이 Q & A를 여러 번 실행했으며보다 포괄적 인 답변을 제공하고자했습니다. 이것에 대해 생각하는 가장 좋은 방법은 호출자에게 오류를 반환 하는 방법 과 반환 하는 것입니다.

어떻게

함수에서 정보를 반환하는 방법은 3 가지가 있습니다.

  1. 반환 값
  2. 인수 없음
  3. 대역 외, 로컬이 아닌 goto (setjmp / longjmp), 파일 또는 전역 범위 변수, 파일 시스템 등이 포함됩니다.

반환 값

반환 값은 단일 객체이지만 임의의 복잡한 것일 수 있습니다. 다음은 오류 반환 함수의 예입니다.

  enum error hold_my_beer();

반환 값의 한 가지 이점은 덜 방해적인 오류 처리를 위해 호출 체인을 허용한다는 것입니다.

  !hold_my_beer() &&
  !hold_my_cigarette() &&
  !hold_my_pants() ||
  abort();

이것은 가독성에 관한 것이 아니라 그러한 함수 포인터의 배열을 균일 한 방식으로 처리 할 수있게합니다.

인수 없음

인수를 통해 둘 이상의 객체를 통해 더 많은 것을 반환 할 수 있지만 최선의 방법은 총 인수 수를 낮게 유지하는 것이 좋습니다 (예 : <= 4).

void look_ma(enum error *e, char *what_broke);

enum error e;
look_ma(e);
if(e == FURNITURE) {
  reorder(what_broke);
} else if(e == SELF) {
  tell_doctor(what_broke);
}

대역 외

setjmp ()를 사용하면 장소와 int 값을 처리 할 방법을 정의하고 longjmp ()를 통해 해당 위치로 제어를 전송합니다. C에서 setjmp 및 longjmp의 실제 사용법을 참조하십시오 .

  1. 지시자
  2. 암호
  3. 목적
  4. 콜백

지시자

오류 표시기는 문제가 있음을 나타내지 만 언급 된 문제의 특성에 대해서는 아무것도 없습니다.

struct foo *f = foo_init();
if(!f) {
  /// handle the absence of foo
}

이것은 함수가 오류 상태를 전달할 수있는 가장 강력한 방법이지만 호출자가 단계적으로 오류에 응답 할 수없는 경우에 적합합니다.

암호

오류 코드는 호출자에게 문제의 특성을 알려주고 적절한 응답을 허용 할 수 있습니다 (위에서). 반환 값이거나 오류 인수 위의 look_ma () 예제와 비슷할 수 있습니다.

목적

오류 개체를 사용하면 임의의 복잡한 문제에 대해 발신자에게 알릴 수 있습니다. 예를 들어, 오류 코드 및 적합한 사람이 읽을 수있는 메시지입니다. 또한 호출자에게 여러 가지 문제가 발생했거나 컬렉션을 처리 할 때 항목 당 오류가 있음을 알릴 수 있습니다.

struct collection friends;
enum error *e = malloc(c.size * sizeof(enum error));
...
ask_for_favor(friends, reason);
for(int i = 0; i < c.size; i++) {
   if(reason[i] == NOT_FOUND) find(friends[i]);
}

오류 배열을 사전 할당하는 대신 물론 필요에 따라 동적으로 (재) 할당 할 수도 있습니다.

콜백

콜백은 오류를 처리하는 가장 강력한 방법입니다. 함수에 문제가 발생했을 때 어떤 동작이 발생하는지 알 수 있습니다. 각 함수에 콜백 인수를 추가하거나 다음과 같이 구조체의 인스턴스 당 사용자 정의 UI 만 필요한 경우 :

 struct foo {
    ...
    void (error_handler)(char *);
 };

 void default_error_handler(char *message) { 
    assert(f);
    printf("%s", message);
 }

 void foo_set_error_handler(struct foo *f, void (*eh)(char *)) {
    assert(f);
    f->error_handler = eh;
 }

 struct foo *foo_init() {
    struct foo *f = malloc(sizeof(struct foo));
    foo_set_error_handler(f, default_error_handler);
    return f;
 }


 struct foo *f = foo_init();
 foo_something();

콜백의 한 가지 흥미로운 이점은 여러 번 호출 할 수 있거나 행복한 경로에 오버 헤드가없는 오류가없는 경우에는 전혀 호출 할 수 없다는 것입니다.

그러나 제어의 반전이 있습니다. 호출 코드는 콜백이 호출되었는지 알 수 없습니다. 따라서 표시기를 사용하는 것이 좋습니다.


1

편집 : 마지막 오류에만 액세스해야하고 멀티 스레드 환경에서는 작동하지 않습니다.

true / false (또는 C에서 작업하고 bool 변수를 지원하지 않는 경우 일종의 #define) 만 반환하고 마지막 오류를 보유하는 전역 오류 버퍼를 가질 수 있습니다.

int getObjectSize(MYAPIHandle h, int* returnedSize);
MYAPI_ERROR LastError;
MYAPI_ERROR* getLastError() {return LastError;};
#define FUNC_SUCCESS 1
#define FUNC_FAIL 0

if(getObjectSize(h, &size) != FUNC_SUCCESS ) {
    MYAPI_ERROR* error = getLastError();
    // error handling
}

실제로 C가 아닌 경우 OS에서 제공하거나 제공하지 않을 수 있습니다. 예를 들어 실시간 운영 체제에서 작업하는 경우에는 운영 체제가 없을 수 있습니다.
Ilya

1

두 번째 방법은 변수의 주소가 함수에 전달 될 때 컴파일러가 다른 함수에 대한 후속 호출 중에 레지스터에 값을 유지할 수 없기 때문에 컴파일러가보다 최적화 된 코드를 생성 할 수 있도록합니다. 완료 코드는 일반적으로 호출 직후 한 번만 사용되지만 호출에서 반환 된 "실제"데이터는 더 자주 사용될 수 있습니다.


1

다음 기술을 사용하여 C에서 오류 처리를 선호합니다.

struct lnode *insert(char *data, int len, struct lnode *list) {
    struct lnode *p, *q;
    uint8_t good;
    struct {
            uint8_t alloc_node : 1;
            uint8_t alloc_str : 1;
    } cleanup = { 0, 0 };

   // allocate node.
    p = (struct lnode *)malloc(sizeof(struct lnode));
    good = cleanup.alloc_node = (p != NULL);

   // good? then allocate str
    if (good) {
            p->str = (char *)malloc(sizeof(char)*len);
            good = cleanup.alloc_str = (p->str != NULL);
    }

   // good? copy data
    if(good) {
            memcpy ( p->str, data, len );
    }

   // still good? insert in list
    if(good) {
            if(NULL == list) {
                    p->next = NULL;
                    list = p;
            } else {
                    q = list;
                    while(q->next != NULL && good) {
                            // duplicate found--not good
                            good = (strcmp(q->str,p->str) != 0);
                            q = q->next;
                    }
                    if (good) {
                            p->next = q->next;
                            q->next = p;
                    }
            }
    }

   // not-good? cleanup.
    if(!good) {
            if(cleanup.alloc_str)   free(p->str);
            if(cleanup.alloc_node)  free(p);
    }

   // good? return list or else return NULL
    return (good ? list : NULL);
}

출처 : http://blog.staila.com/?p=114


1
좋은 기술. 나는 goto반복되는 대신에 더 깔끔한 것을 발견 if합니다. 참고 : 하나 , .
Ant_222

0

또한 다른 훌륭한 답변 외에도 각 호출마다 한 줄을 저장하기 위해 오류 플래그와 오류 코드를 분리하는 것이 좋습니다.

if( !doit(a, b, c, &errcode) )
{   (* handle *)
    (* thine  *)
    (* error  *)
}

많은 오류 점검이있을 때이 작은 단순화가 실제로 도움이됩니다.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.