Any good idioms for error handling in straight C programs?

后端 未结 12 1511
梦如初夏
梦如初夏 2020-12-23 14:19

Getting back in to some C work.

Many of my functions look like this:

int err = do_something(arg1, arg2, arg3, &result);

With th

相关标签:
12条回答
  • 2020-12-23 14:49

    If you have resources that need to be released at the end, then sometimes the old trusty goto can be handy!

    int
    major_func(size_t len)
    {
        int err;
        char *buf;
    
        buf = malloc(len);
    
        if (err = minor_func1(buf))
            goto major_func_end;
        if (err = minor_func2(buf))
            goto major_func_end;
        if (err = minor_func3(buf))
            goto major_func_end;
    
    major_func_end:
        free(buf);
        return err;
    }
    
    0 讨论(0)
  • 2020-12-23 14:50

    And now for something completely different...

    Another approach is to use a struct to contain your error information, e.g:

    struct ErrorInfo
    {
        int errorCode;
        char *errorMessage;
    #if DEBUG
        char *functionName;
        int lineNumber;
    #endif
    }
    

    The best way to use this is to return your method's results as the return code (e.g. "FALSE for failed", or "a file pointer or NULL if it fails", or "size of the buffer or 0 if it fails", etc) and pass in an ErrorInfo as a parameter that the called function will fill in if something fails.

    This gives rich error reporting: if the method fails, you can fill in more than a simple error code (e.g. error message, code line and file of the failure, or whatever). The nice thing about it being a struct is that if you think of something, anything, useful later, you can just add it - for example, in my struct above I've allowed for a debug build to include the location of the error (file/line), but you could add a dump of the whole call stack in there at any time without having to change any of the client code.

    You can use a global function to fill in an ErrorInfo so the error return can be managed cleanly, and you can update the struct to provide more info easily:

    if (error)
    {
        Error(pErrorInfo, 123, "It failed");
        return(FALSE);
    }
    

    ...and you can have variants of this function that return FALSE, 0, or NULL, to allow most error returns to be phrased as a single line:

    if (error)
        return(ErrorNull(pErrorInfo, 123, "It failed"));
    

    This gives you a lot of the advantages of an Exception class in other languages (although the caller still needs to handle the errors - callers have to check for error codes and may have to return early, but they can do nothing or next-to-nothing and allow the error to propagate back up a chain of calling methods until one of them wishes to handle it, much like an exception.

    In addition, you can go further, to create a chain of error reports (like "InnerException"s):

    struct ErrorInfo
    {
        int errorCode;
        char *errorMessage;
        ...
        ErrorInfo *pInnerError;    // Pointer to previous error that may have led to this one
    }
    

    Then, if you "catch" an error from a function that you call, you can create a new, higher-level error description, and return a chain of these errors. e.g. "Mouse speed will revert to the default value" (because) "Preference block 'MousePrefs' could not be located" (because) "XML reader failed" (because) "File not found".

    i.e.

    FILE *OpenFile(char *filename, ErrorInfo *pErrorInfo)
    {
        FILE *fp = fopen(filename, "rb");
        if (fp == NULL)
            return(ChainedErrorNull(pErrorInfo, "Couldn't open file"));
    
        return(fp);
    }
    
    XmlElement *ReadPreferenceXml(ErrorInfo *pErrorInfo)
    {
        if (OpenFile("prefs.xml", pErrorInfo) == NULL)
            return(ChainedErrorNull(pErrorInfo, "Couldn't read pref"));
        ...
    }
    
    char *ReadPreference(char *prefName, ErrorInfo *pErrorInfo)
    {
        XmlElement *pXml = ReadPreferenceXml(pErrorInfo);
        if (pXml == NULL)
            return(ChainedErrorNull(pErrorInfo, "Couldn't read pref"));
        ...
    }
    
    0 讨论(0)
  • 2020-12-23 14:55

    Others have suggested good ideas. Here're the idioms I've seen

    int err;
    ...
    err = foo(...);
    if (err)
        return err;
    ...
    

    You could macro this out to something like

    #define dERR int err=0
    #define CALL err = 
    #define CHECK do { if (err) return err } while(0)
    ...
    void my_func(void) {
       dERR;
       ...
       CALL foo(...);
       CHECK;
    

    or, if you're feeling really motivated, fiddle with CALL and CHECK so they can be used like

    CALL foo(...) CHECK;
    

    or

    CALL( foo(...) );
    

    --

    Often, functions which need to do cleanup on exit (e.g. free memory) are written like this:

    int do_something_complicated(...) {
        ...
    
        err = first_thing();
        if (err)
           goto err_out;
    
        buffer = malloc(...);
        if (buffer == NULL)
            goto err_out
    
        err = another_complicated(...);
        if (err)
            goto err_out_free;
    
        ...
    
       err_out_free:
        free(buffer);
       err_out:
        return err; /* err might be zero */
    }
    

    You could use that pattern, or try to simplify it with macros.

    --

    Finally, if you're feeling /really/ motivated, you can use setjmp/longjmp.

    int main(int argc, char *argv[]) {
        jmp_buf on_error;
        int err;
        if (err = setjmp(on_error)) {
            /* error occurred, error code in err */
            return 1;
        } else {
            actual_code(..., on_error);
            return 0;
        }
    }
    void actual_code(..., jmp_buf on_error) {
        ...
        if (err)
            longjmp(on_error, err);
    }
    

    Essentially, a declaration of a new jmp_buf and a setjmp function as setting up a try block. The case where setjmp returns non-zero is your catch, and calling longjmp is your throw. I wrote this with passing the jmp_buf around in case you want nested handlers (e.g. if you need to free stuff before signaling an error); if you don't need that, feel free to declare err and the jmp_buf as globals.

    Alternately, you could use macros to simply the argument passing around. I'd suggest the way Perl's implementation does it:

    #define pERR jmp_buf _err_handler
    #define aERR _err_handler
    #define HANDLE_ERRORS do { jmp_buf _err_handler; int err = setjmp(_err_handler);
    #define END_HANDLE while(0)
    #define TRY if (! err)
    #define CATCH else
    #define THROW(e) longjmp(_err_handler, e)
    
    void always_fails(pERR, int other_arg) {
        THROW(42);
    }
    void does_some_stuff(pERR) {
        normal_call(aERR);
        HANDLE_ERRORS
          TRY {
            always_fails(aERR, 23);
          } CATCH {
            /* err is 42 */
          }
        END_HANDLE;
    }
    int main(int argc, char *argv[]) {
        HANDLE_ERRORS
          TRY {
            does_some_stuff(aERR);
            return 0;
          } CATCH {
            return err;
          }
        DONE_ERRORS;
    }
    

    --

    Phew. I'm done. (Crazy examples untested. Some details might be off.)

    0 讨论(0)
  • 2020-12-23 14:55

    Provided you are working with a specific context, I think the following pattern is very nice. The basic idea is that operations on an error-set state are no-ops, so error checking can be postponed to when it is convenient!

    A concrete example: A deserialization context. Decoding of any element can fail, but the function may continue without error checking because all the decode_* functions are no-ops when the serialization record is in an error state. It's a matter of convenience or opportunity or optimization to insert decode_has_error. In the example below, there is no error check, the caller will take care of that.

    void list_decode(struct serialization_record *rec,                       
                     struct list *list,                                     
                     void *(*child_decode)(struct serialization_record *)) {
        uint32_t length;                                                             
        decode_begin(rec, TAG);                                  
        decode_uint32(rec, &length);                                          
        for (uint32_t i = 0; i < length; i++) {                                
            list_append(list, child_decode(rec));
        }                                                                        
        decode_end(rec, TAG);
    }
    
    0 讨论(0)
  • 2020-12-23 14:56

    If error codes are boolean, then try the simpler code below:

    return func1() && func2() && func3()
    
    0 讨论(0)
  • 2020-12-23 14:58

    You can get really silly and do continuations:

    void step_1(int a, int b, int c, void (*step_2)(int), void (*err)(void *) ) {
         if (!c) {
             err("c was 0");
         } else {
             int r = a + b/c;
             step_2(r);
         }
    }
    

    This probably isn't actually what you want to do, but it is how many functional programming languages are used, and even more often how they model their code for optimization.

    0 讨论(0)
提交回复
热议问题