Obsługa błędów w kodzie C.


152

Co uważasz za „najlepszą praktykę”, jeśli chodzi o obsługę błędów w spójny sposób w bibliotece C.

Są dwa sposoby, o których myślałem:

Zawsze zwracaj kod błędu. Typowa funkcja wyglądałaby tak:

MYAPI_ERROR getObjectSize(MYAPIHandle h, int* returnedSize);

Zawsze zapewniają podejście do wskaźnika błędu:

int getObjectSize(MYAPIHandle h, MYAPI_ERROR* returnedError);

Korzystając z pierwszego podejścia, można napisać taki kod, w którym kontrola obsługi błędów jest bezpośrednio umieszczana w wywołaniu funkcji:

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

Co wygląda lepiej niż kod obsługi błędów tutaj.

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

Jednak myślę, że użycie wartości zwracanej do zwracania danych sprawia, że ​​kod jest bardziej czytelny. Jest oczywiste, że w drugim przykładzie coś zostało zapisane w zmiennej rozmiaru.

Czy masz jakieś pomysły, dlaczego powinienem preferować któreś z tych podejść, a może mieszać je lub używać czegoś innego? Nie jestem fanem globalnych stanów błędów, ponieważ sprawia to, że wielowątkowe korzystanie z biblioteki jest o wiele bardziej bolesne.

EDYCJA: konkretne pomysły C ++ na ten temat również byłyby interesujące do usłyszenia, o ile nie zawierają wyjątków, ponieważ w tej chwili nie jest to dla mnie opcja ...


Uczyłem się języka C dopiero od około dwóch tygodni, ale mam wrażenie, że parametry OUT są wartością zwracaną przez defacto dla większości funkcji, ponieważ pozwala to uniknąć narzutu zwracania struktur według wartości i łagodzi potrzeba zwolnienia pamięci, ponieważ większość zmiennych znajduje się na stosie. Ponieważ więc nie używam wyrażenia „return” jako rzeczywistej wartości funkcji, mogę go używać do obsługi błędów przez większość czasu.
Joel Roberts

Odpowiedzi:


74

Podoba mi się błąd jako sposób zwracania wartości. Jeśli projektujesz interfejs API i chcesz maksymalnie bezboleśnie korzystać z biblioteki, pomyśl o tych dodatkach:

  • przechowuj wszystkie możliwe stany błędów w jednym typie wyliczenia i używaj go w swojej bibliotece. Nie zwracaj tylko liczb całkowitych lub co gorsza, mieszaj wartości całkowite lub różne wyliczenia z kodami powrotu.

  • udostępniają funkcję, która przekształca błędy w coś czytelnego dla człowieka. Może być proste. Po prostu wyliczenie błędu, const char * out.

  • Wiem, że ten pomysł nieco utrudnia korzystanie z wielowątkowości, ale byłoby miło, gdyby programista aplikacji mógł ustawić globalne wywołanie zwrotne błędu. W ten sposób będą mogli umieścić punkt przerwania w wywołaniu zwrotnym podczas sesji wyszukiwania błędów.

Mam nadzieję, że to pomoże.


5
Dlaczego mówisz: „ta idea utrudnia korzystanie z wielu wątków”. Która część jest utrudniona przez wielowątkowość? Czy możesz podać szybki przykład?
SayeedHussain

1
@crypticcoder Mówiąc prosto: globalne wywołanie zwrotne błędu można wywołać w dowolnym kontekście wątku. Jeśli po prostu wydrukujesz błąd, nie napotkasz żadnych problemów. Jeśli spróbujesz naprawić problemy, będziesz musiał dowiedzieć się, który wątek wywołujący spowodował błąd, a to utrudnia.
Nils Pipenbrinck

9
A jeśli chcesz przekazać więcej szczegółów o błędzie? Np. Masz błąd parsera i chcesz podać numer wiersza i kolumnę błędu składniowego oraz sposób, aby ładnie to wszystko wydrukować.
panzi

1
@panzi wtedy oczywiście musisz zwrócić strukturę (lub użyć wskaźnika out, jeśli struktura jest naprawdę duża) i mieć funkcję formatującą strukturę jako łańcuch.
Skrzydłowy Sendon


92

Użyłem obu podejść i oba działały dobrze dla mnie. Niezależnie od tego, którego używam, zawsze staram się stosować tę zasadę:

Jeśli jedynymi możliwymi błędami są błędy programisty, nie zwracaj kodu błędu, użyj asserts wewnątrz funkcji.

Twierdzenie, które weryfikuje dane wejściowe, jasno komunikuje, czego oczekuje funkcja, podczas gdy zbyt częste sprawdzanie błędów może zaciemniać logikę programu. Podjęcie decyzji, co zrobić dla wszystkich różnych przypadków błędów, może naprawdę skomplikować projekt. Po co zastanawiać się, jak functionX ma obsługiwać wskaźnik zerowy, jeśli zamiast tego można nalegać, aby programista nigdy go nie przekazał?


1
Masz przykład potwierdzeń w języku C? (Jestem bardzo zielony do C)
thomthom

Zwykle jest to tak proste, jak assert(X)gdzie X jest dowolną poprawną instrukcją C, która ma być prawdziwa. zobacz stackoverflow.com/q/1571340/10396 .
AShelly

14
Ugh, absolutnie nigdy nie używaj potwierdzeń w kodzie biblioteki ! Ponadto, nie mieszają się różne style obsługi błędów w jednym kawałku kodu jak inni zrobili ...
mirabilos

10
Z pewnością zgadzam się z nie mieszaniem stylów. Ciekawi mnie twoje rozumowanie twierdzeń. Jeśli dokumentacja funkcji mówi, że „argument X nie może mieć wartości NULL” lub „Y musi być członkiem tego wyliczenia”, to co jest nie tak z assert(X!=NULL);lub assert(Y<enumtype_MAX);? Zobacz tę odpowiedź na stronie programistów i pytanie, do którego prowadzi, aby uzyskać więcej informacji na temat tego, dlaczego uważam, że jest to właściwa droga.
AShelly

8
@AShelly Problem z zapewnieniem, że zwykle nie ma ich w kompilacjach wydań.
Calmarius

29

Jest ładny zestaw slajdów z CMU CERT z zaleceniami, kiedy należy używać każdej z typowych technik obsługi błędów C (i C ++). Jednym z najlepszych slajdów jest to drzewo decyzyjne:

Błąd obsługi drzewa decyzyjnego

Osobiście zmieniłbym dwie rzeczy w tym schemacie blokowym.

Po pierwsze, chciałbym wyjaśnić, że czasami obiekty powinny używać wartości zwracanych do wskazywania błędów. Jeśli funkcja tylko wyodrębnia dane z obiektu, ale nie powoduje jego mutacji, integralność samego obiektu nie jest zagrożona i bardziej odpowiednie jest wskazywanie błędów przy użyciu wartości zwracanej.

Po drugie, nie zawsze jest właściwe używanie wyjątków w C ++. Wyjątki są dobre, ponieważ mogą zmniejszyć ilość kodu źródłowego przeznaczonego na obsługę błędów, przeważnie nie wpływają na sygnatury funkcji i są bardzo elastyczne w zakresie danych, które mogą przepuścić stos wywołań. Z drugiej strony wyjątki mogą nie być właściwym wyborem z kilku powodów:

  1. Wyjątki w C ++ mają bardzo szczególną semantykę. Jeśli nie chcesz tej semantyki, wyjątki C ++ są złym wyborem. Wyjątek należy rozpatrywać natychmiast po rzuceniu, a projekt faworyzuje przypadek, w którym błąd będzie wymagał rozwinięcia stosu wywołań o kilka poziomów.

  2. Funkcje C ++, które generują wyjątki, nie mogą być później opakowane, aby nie zgłaszały wyjątków, a przynajmniej nie bez płacenia pełnego kosztu wyjątków. Funkcje, które zwracają kody błędów, mogą być opakowane w celu wyrzucania wyjątków C ++, co czyni je bardziej elastycznymi. C ++ newrobi to dobrze, udostępniając wariant, który nie rzuca.

  3. Wyjątki w C ++ są stosunkowo drogie, ale ta wada jest przeważnie przesadzona w przypadku programów, które rozsądnie wykorzystują wyjątki. Program po prostu nie powinien generować wyjątków w ścieżce kodowej, w której liczy się wydajność. Naprawdę nie ma znaczenia, jak szybko Twój program może zgłosić błąd i zakończyć pracę.

  4. Czasami wyjątki C ++ nie są dostępne. Albo dosłownie nie są dostępne w implementacji C ++, albo wytyczne dotyczące kodu ich zabraniają.


Ponieważ oryginalne pytanie było o wielowątkowym kontekście myślę lokalny technika wskaźnik błędu (co opisano w SirDarius „s odpowiedzi ) był niedoceniany w oryginalnych odpowiedzi. Jest bezpieczny wątkowo, nie wymusza natychmiastowego usunięcia błędu przez dzwoniącego i może łączyć dowolne dane opisujące błąd. Wadą jest to, że musi być przechowywany przez obiekt (lub przypuszczam, że jest w jakiś sposób powiązany zewnętrznie) i jest prawdopodobnie łatwiejszy do zignorowania niż kod powrotu.


5
Możesz zauważyć, że standardy kodowania C ++ firmy Google nadal mówią, że nie używamy wyjątków C ++.
Jonathan Leffler

19

Pierwszego podejścia używam zawsze, gdy tworzę bibliotekę. Istnieje kilka zalet używania wyliczenia o typie zdefiniowanym jako kodu powrotu.

  • Jeśli funkcja zwraca bardziej skomplikowane dane wyjściowe, takie jak tablica i jej długość, nie musisz tworzyć dowolnych struktur do zwrócenia.

    rc = func(..., int **return_array, size_t *array_length);
  • Pozwala na prostą, znormalizowaną obsługę błędów.

    if ((rc = func(...)) != API_SUCCESS) {
       /* Error Handling */
    }
  • Pozwala na prostą obsługę błędów w funkcji bibliotecznej.

    /* Check for valid arguments */
    if (NULL == return_array || NULL == array_length)
        return API_INVALID_ARGS;
  • Użycie wyliczenia wpisanego w typedef umożliwia również wyświetlenie nazwy wyliczenia w debugerze. Pozwala to na łatwiejsze debugowanie bez konieczności ciągłego sprawdzania pliku nagłówkowego. Posiadanie funkcji do przetłumaczenia tego wyliczenia na ciąg jest również pomocne.

Najważniejszą kwestią niezależnie od zastosowanego podejścia jest konsekwencja. Dotyczy to nazewnictwa funkcji i argumentów, kolejności argumentów i obsługi błędów.


9

Użyj 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
Drugi blok kodu jest oparty na wcześniejszej wersji kodu na stronie Francesco Nidito, do której odwołuje się u góry odpowiedzi. ETRYKod został zmieniony, ponieważ ta odpowiedź została napisana.
Jonathan Leffler

2
Setjmp to straszna strategia obsługi błędów. Jest drogie, podatne na błędy (w / nieulotne zmienione lokacje nie zachowują swoich zmienionych wartości i wszystkich) i powoduje wycieki zasobów, jeśli przydzielisz je pomiędzy wywołaniami setjmp i longjmp. Powinieneś być w stanie wykonać około 30 zwrotów i kontroli int-val, zanim odzyskasz koszt sigjmp / longjmp. Większość stosów wywołań nie jest tak głęboka, zwłaszcza jeśli nie używasz zbyt dużej liczby rekurencji (a jeśli tak, masz problemy z perfekcją inne niż koszt zwrotów + czeków).
PSkocik

1
Jeśli zepsujesz pamięć, a następnie rzucisz, pamięć po prostu wycieknie na zawsze. setjmpJest również drogi, nawet jeśli nigdy nie zostanie zgłoszony żaden błąd, pochłonie sporo czasu procesora i miejsca w stosie. Używając gcc dla Windows, możesz wybierać pomiędzy różnymi metodami obsługi wyjątków dla C ++, jedna z nich bazuje na setjmpiw praktyce spowalnia twój kod nawet o 30%.
Mecki

7

Osobiście wolę to pierwsze podejście (zwracanie wskaźnika błędu).

Tam, gdzie to konieczne, wynik zwrotu powinien po prostu wskazywać, że wystąpił błąd, a inna funkcja jest używana do znalezienia dokładnego błędu.

W twoim przykładzie getSize () uważam, że rozmiary zawsze muszą być równe zero lub dodatnie, więc zwrócenie wyniku ujemnego może wskazywać na błąd, podobnie jak wywołania systemowe UNIX.

Nie przychodzi mi do głowy żadna biblioteka, z której korzystałem, która obsługuje to drugie podejście z obiektem błędu przekazanym jako wskaźnik. stdioitp. wszystkie mają wartość zwracaną.


1
Dla przypomnienia, jedną biblioteką, którą widziałem używającą tego drugiego podejścia, jest API programowania Maya. Jest to jednak biblioteka C ++, a nie C. Jest dość niespójny w sposobie obsługi błędów i czasami błąd jest przekazywany jako wartość zwracana, a innym razem przekazuje wynik jako odniesienie.
Laserallan

1
nie zapomnij o strtod, ok, ostatni argument służy nie tylko do wskazywania błędów, ale też to robi.
quinmars

7

Kiedy piszę programy, podczas inicjalizacji zwykle wyodrębniam wątek do obsługi błędów i inicjalizuję specjalną strukturę dla błędów, w tym blokadę. Następnie, gdy wykryję błąd, poprzez wartości zwracane, wprowadzam informacje z wyjątku do struktury i wysyłam SIGIO do wątku obsługi wyjątków, a następnie sprawdzam, czy nie mogę kontynuować wykonywania. Jeśli nie mogę, wysyłam SIGURG do wątku wyjątku, który z wdziękiem zatrzymuje program.


7

Zwracanie kodu błędu jest typowym podejściem do obsługi błędów w C.

Ale ostatnio eksperymentowaliśmy również ze wskaźnikiem błędu wychodzącego.

Ma kilka zalet w porównaniu z podejściem wartości zwracanej:

  • Możesz użyć wartości zwracanej do bardziej znaczących celów.

  • Konieczność zapisania tego parametru błędu przypomina o obsłużeniu błędu lub jego propagacji. (Nigdy nie zapomnisz sprawdzenia wartości zwracanej fclose, prawda?)

  • Jeśli używasz wskaźnika błędu, możesz go przekazać podczas wywoływania funkcji. Jeśli któraś z funkcji ją ustawi, wartość nie zostanie utracona.

  • Ustawiając punkt przerwania danych w zmiennej błędu, można określić, gdzie wystąpił błąd. Ustawiając warunkowy punkt przerwania, możesz również wychwycić określone błędy.

  • Ułatwia zautomatyzowanie kontroli, czy obsługujesz wszystkie błędy. Konwencja kodu może zmusić Cię do wywołania wskaźnika błędu jako erri musi to być ostatni argument. Więc skrypt może dopasować ciąg, err);a następnie sprawdzić, czy następuje po nim if (*err. Właściwie w praktyce utworzyliśmy makro o nazwie CER(check err return) i CEG(check err goto). Nie musisz więc wpisywać tego zawsze, gdy chcemy po prostu zwrócić błąd, i możesz zmniejszyć wizualny bałagan.

Jednak nie wszystkie funkcje w naszym kodzie mają ten parametr wychodzący. Ten parametr wychodzący jest używany w przypadkach, w których normalnie wyrzucasz wyjątek.


6

W przeszłości wiele razy programowałem w C. I naprawdę doceniłem wartość zwracaną przez kod błędu. Ale ma kilka możliwych pułapek:

  • Powielone numery błędów, można to rozwiązać za pomocą globalnego pliku errors.h.
  • Zapominając o sprawdzeniu kodu błędu, należy to rozwiązać za pomocą wskazówki i długich godzin debugowania. Ale w końcu się nauczysz (lub będziesz wiedział, że ktoś inny wykona debugowanie).

2
Drugi problem można rozwiązać poprzez odpowiedni poziom ostrzegania kompilatora, odpowiedni mechanizm przeglądu kodu oraz narzędzia do statycznego analizowania kodu.
Ilya

1
Możesz także pracować na zasadzie: jeśli wywoływana jest funkcja API, a wartość zwracana nie jest sprawdzana, jest błąd.
Jonathan Leffler

6

Podejście UNIX jest najbardziej podobne do twojej drugiej sugestii. Zwróć wynik lub pojedynczą wartość „poszło źle”. Na przykład open zwróci deskryptor pliku w przypadku sukcesu lub -1 w przypadku niepowodzenia. W przypadku niepowodzenia ustawia również errnozewnętrzną globalną liczbę całkowitą wskazującą, który błąd wystąpił.

Co jest warte, Cocoa również stosuje podobne podejście. Wiele metod zwraca wartość BOOL i przyjmuje NSError **parametr, więc w przypadku niepowodzenia ustawia błąd i zwraca wartość NO. Wtedy obsługa błędów wygląda następująco:

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

co jest gdzieś pomiędzy twoimi dwiema opcjami :-).



5

Oto podejście, które uważam za interesujące, ale wymagające pewnej dyscypliny.

Zakłada się, że zmienna typu uchwyt jest instancją, na której działają wszystkie funkcje API.

Chodzi o to, że struktura za uchwytem przechowuje poprzedni błąd jako strukturę z niezbędnymi danymi (kod, komunikat ...), a użytkownik ma funkcję, która zwraca wskaźnik do tego obiektu błędu. Każda operacja zaktualizuje wskazany obiekt, dzięki czemu użytkownik będzie mógł sprawdzić jego stan bez wywoływania funkcji. W przeciwieństwie do wzorca errno kod błędu nie jest globalny, co sprawia, że ​​podejście jest bezpieczne dla wątków, o ile każdy uchwyt jest prawidłowo używany.

Przykład:

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

Pierwsze podejście jest lepsze IMHO:

  • W ten sposób łatwiej jest napisać funkcję. Kiedy zauważysz błąd w środku funkcji, po prostu zwracasz wartość błędu. W drugim podejściu musisz przypisać wartość błędu do jednego z parametrów, a następnie coś zwrócić ... ale co byś zwrócił - nie masz poprawnej wartości i nie zwracasz wartości błędu.
  • jest bardziej popularny, więc będzie łatwiejszy do zrozumienia, utrzymania

4

Zdecydowanie wolę pierwsze rozwiązanie:

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

nieznacznie zmodyfikowałbym to, aby:

int size;
MYAPIError rc;

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

Ponadto nigdy nie będę mieszał prawidłowej wartości zwracanej z błędem, nawet jeśli obecnie zakres funkcji na to pozwala, nigdy nie wiadomo, w którą stronę pójdzie implementacja funkcji w przyszłości.

A jeśli już mówimy o obsłudze błędów, sugerowałbym goto Error;jako kod obsługi błędów, chyba że undomożna wywołać jakąś funkcję do poprawnej obsługi błędów.


3

To, co możesz zrobić zamiast zwracać swój błąd, a tym samym zabraniać zwracania danych za pomocą funkcji, to użycie opakowania dla zwracanego typu:

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

Następnie w wywołanej funkcji:

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

Zauważ, że w poniższej metodzie opakowanie będzie miało rozmiar MyType plus jeden bajt (w większości kompilatorów), co jest dość opłacalne; i nie będziesz musiał odkładać kolejnego argumentu na stos, gdy wywołasz swoją funkcję ( returnedSizelub returnedErrorw obu przedstawionych metodach).


3

Oto prosty program do zademonstrowania pierwszych 2 punktów odpowiedzi Nilsa Pipenbrincka .

Jego pierwsze 2 kule to:

  • przechowuj wszystkie możliwe stany błędów w jednym typie wyliczenia i używaj go w swojej bibliotece. Nie zwracaj tylko liczb całkowitych lub co gorsza, mieszaj wartości całkowite lub różne wyliczenia z kodami powrotu.

  • udostępniają funkcję, która przekształca błędy w coś czytelnego dla człowieka. Może być proste. Po prostu wyliczenie błędu, const char * out.

Załóżmy, że napisałeś moduł o nazwie mymodule. Najpierw w mymodule.h definiujesz kody błędów oparte na wyliczeniach i piszesz kilka ciągów błędów, które odpowiadają tym kodom. Tutaj używam tablicy ciągów C ( char *), która działa dobrze tylko wtedy, gdy pierwszy kod błędu oparty na wyliczeniu ma wartość 0, a następnie nie manipulujesz liczbami. Jeśli używasz numerów kodów błędów z przerwami lub innymi wartościami początkowymi, będziesz musiał po prostu zmienić używanie odwzorowanej tablicy ciągów C (tak jak robię poniżej) na użycie funkcji, która używa instrukcji switch lub instrukcji if / else if mapować z kodów błędów wyliczenia na drukowalne ciągi C (których nie demonstruję). Wybór nalezy do ciebie.

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 zawiera moją funkcję mapującą do mapowania kodów błędów wyliczenia na drukowane ciągi 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 zawiera program testowy pokazujący wywoływanie niektórych funkcji i wypisywanie z nich niektórych kodów błędów:

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

Wynik:

Demonstracja kodów błędów opartych na wyliczeniach w C (lub C ++)
kodzie błędu z mymodule_func1 () = MYMODULE_ERROR_OK
kod błędu z mymodule_func2 () = MYMODULE_ERROR_INVARG
Kod błędu z mymodule_func3 () = MYMODUMLE_ERR

Bibliografia:

Możesz sam uruchomić ten kod tutaj: https://onlinegdb.com/ByEbKLupS .


2

Oprócz tego, co zostało powiedziane, przed zwróceniem kodu błędu uruchom asercję lub podobną diagnostykę po zwróceniu błędu, ponieważ znacznie ułatwi to śledzenie. Sposób, w jaki to robię, polega na tym, aby mieć dostosowane potwierdzenie, które nadal jest kompilowane w momencie wydania, ale jest uruchamiane tylko wtedy, gdy oprogramowanie jest w trybie diagnostycznym, z opcją cichego raportowania do pliku dziennika lub wstrzymania na ekranie.

Osobiście zwracam kody błędów jako ujemne liczby całkowite z wartością no_error równą zero, ale powoduje to możliwy następujący błąd

if (MyFunc())
 DoSomething();

Alternatywą jest sytuacja, w której błąd zawsze zwracany jest jako zero i użyj funkcji LastError (), aby podać szczegóły rzeczywistego błędu.


2

Wiele razy spotkałem się z tym pytaniem i odpowiedziami i chciałem udzielić bardziej wyczerpującej odpowiedzi. Myślę, że najlepszym sposobem, aby myśleć o to w jaki sposób , aby powrócić do rozmówcy błędów, a co wrócisz.

W jaki sposób

Istnieją 3 sposoby zwracania informacji z funkcji:

  1. Wartość zwracana
  2. Nasze argumenty
  3. Poza pasmem, które obejmuje nielokalne goto (setjmp / longjmp), zmienne o zasięgu plikowym lub globalnym, system plików itp.

Wartość zwracana

Możesz zwrócić tylko wartość jako pojedynczy obiekt, jednak może to być dowolny zespół złożony. Oto przykład funkcji zwracającej błąd:

  enum error hold_my_beer();

Jedną z zalet zwracanych wartości jest to, że umożliwia tworzenie łańcuchów wywołań w celu mniej inwazyjnej obsługi błędów:

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

Nie chodzi tylko o czytelność, ale może również umożliwić przetwarzanie tablicy takich wskaźników funkcji w jednolity sposób.

Nasze argumenty

Możesz zwrócić więcej za pośrednictwem więcej niż jednego obiektu za pomocą argumentów, ale najlepsza praktyka sugeruje utrzymywanie całkowitej liczby argumentów na niskim poziomie (powiedzmy <= 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);
}

Poza pasmem

Za pomocą setjmp () definiujesz miejsce i sposób obsługi wartości int, a także przekazujesz kontrolę do tej lokalizacji za pomocą longjmp (). Zobacz praktycznego wykorzystania setjmp i longjmp w C .

Co

  1. Wskaźnik
  2. Kod
  3. Obiekt
  4. Oddzwonić

Wskaźnik

Wskaźnik błędu informuje tylko, że wystąpił problem, ale nic o jego naturze:

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

Jest to najmniej skuteczny sposób komunikowania przez funkcję stanu błędu, jednak doskonały, jeśli dzwoniący i tak nie może odpowiedzieć na błąd w sposób stopniowy.

Kod

Kod błędu informuje dzwoniącego o naturze problemu i może pozwolić na odpowiednią odpowiedź (z powyższego). Może to być wartość zwracana lub jak przykład look_ma () powyżej argumentu błędu.

Obiekt

W przypadku obiektu błędu dzwoniący może zostać poinformowany o dowolnych skomplikowanych problemach. Na przykład kod błędu i odpowiedni komunikat czytelny dla człowieka. Może również poinformować dzwoniącego, że wiele rzeczy poszło nie tak lub o błędzie na element podczas przetwarzania kolekcji:

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

Zamiast wstępnie przydzielać tablicę błędów, możesz oczywiście (ponownie) alokować ją dynamicznie w razie potrzeby.

Oddzwonić

Callback to najpotężniejszy sposób obsługi błędów, ponieważ możesz powiedzieć funkcji, jakie zachowanie chciałbyś zobaczyć, gdy coś pójdzie nie tak. Argument wywołania zwrotnego można dodać do każdej funkcji lub jeśli dostosowanie interfejsu użytkownika jest wymagane tylko dla wystąpienia takiej struktury:

 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();

Jedną z interesujących zalet wywołania zwrotnego jest to, że może być wywoływane wiele razy lub wcale, jeśli nie ma błędów, które nie powodują obciążenia na szczęśliwej ścieżce.

Istnieje jednak odwrócenie kontroli. Kod wywołujący nie wie, czy wywołano wywołanie zwrotne. W związku z tym sensowne może być również użycie wskaźnika.


1

EDYCJA: Jeśli potrzebujesz dostępu tylko do ostatniego błędu, a nie pracujesz w środowisku wielowątkowym.

Możesz zwrócić tylko prawda / fałsz (lub jakiś rodzaj #define, jeśli pracujesz w C i nie obsługujesz zmiennych bool) i masz globalny bufor błędów, który będzie zawierał ostatni błąd:

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
}

Rzeczywiście, ale nie jest to C, może być dostarczane przez system operacyjny lub nie, jeśli na przykład pracujesz na systemach operacyjnych czasu rzeczywistego, nie możesz go mieć.
Ilya,

1

Drugie podejście pozwala kompilatorowi stworzyć bardziej zoptymalizowany kod, ponieważ kiedy adres zmiennej jest przekazywany do funkcji, kompilator nie może zachować jej wartości w rejestrach podczas kolejnych wywołań innych funkcji. Kod zakończenia jest zwykle używany tylko raz, zaraz po wywołaniu, podczas gdy „rzeczywiste” dane zwrócone przez wywołanie mogą być używane częściej


1

Preferuję obsługę błędów w C przy użyciu następującej techniki:

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

Źródło: http://blog.staila.com/?p=114


1
Dobra technika. Uważam, że nawet schludniej z goto„s” zamiast z powtórzeniami if. Referencje: jeden , dwa .
Ant_222

0

Oprócz innych świetnych odpowiedzi sugeruję, abyś spróbował oddzielić flagę błędu i kod błędu, aby zapisać jedną linię dla każdego połączenia, tj .:

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

Kiedy masz dużo sprawdzania błędów, to małe uproszczenie naprawdę pomaga.

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.