7

Consider this function:

int get_result(int *result) {
     int err = 0;
     int number = 0;         

     if (result == NULL) {
         printf("error: null input\n");
         return -1;
     }

     err = get_number(&number);

     if (err != 0) {
         printf("error calling get_number: err = %d\n", err);
         return err;
     }

     err = calculate_result(number, result);

     if (err != 0) {
        printf("error calling get_result: err = %d\n", err);
        return err;
     }

     return err;
}

The real work in this function requires only 3 lines (declare number variable, call get_number(), then call calculate_result()). However, the error checking/handling code bloats this function to 17 lines (give or take, depending on how you count the lines).

At a larger scale, with many calls, and multiple error checks, we bloat the function entirely and make it unreadable and very hard to understand.

What are ways to get around this bloating in C code and maintain readability of the core operation of a function (without sacrificing essential error handling code)?

Akeel
  • 301
  • 1
  • 7
  • 1
    Why are you using `errno` as a typename? – EOF Aug 22 '15 at 16:47
  • well. if you don't print to stderr, and cut the braces, you save 4-6 lines. `c` is like this. actually all languages are like this, you can argue in java you expand a easy call to a try-catch block too. so, you code what you need. – Jason Hu Aug 22 '15 at 16:47
  • you can, actually using macro you save some work, say `#define SAFE_CALL(expr) { errno err = expr; if (err != 0) return err; }`. this wouldn't be too bad if you restrict such usage in your code within a reasonable scope. – Jason Hu Aug 22 '15 at 16:49
  • Believe it or not `goto` might help. See http://stackoverflow.com/a/2411764/10077 – Fred Larson Aug 22 '15 at 16:51
  • @EOF Should have been errno_t. Changed to int to avoid confusion. – Akeel Aug 22 '15 at 16:54
  • @Akeel is `errno_t` provided or not? if not, you shouldn't name your type in `*_t` form since all of them are reserved according to standard. – Jason Hu Aug 22 '15 at 16:56
  • 1
    @HuStmpHrrr: Not according to the C standard. Only the POSIX standard (which may or may not apply here). – Cornstalks Aug 22 '15 at 16:57
  • @Cornstalks hence replaced it with int to avoid detracting from the purpose of the question. – Akeel Aug 22 '15 at 17:00
  • 1
    if the code works then you'll probably have better answers on http://codereview.stackexchange.com/ – phuclv Aug 22 '15 at 17:00
  • @Akeel: Agreed, it's just a red herring people like to nitpick. – Cornstalks Aug 22 '15 at 17:01
  • after all, it's a good question from the view of a software architect. Using `int` for error codes is common practice in [tag:c], so glad you fixed that. –  Aug 22 '15 at 17:20
  • The error conditions in your example could easily be assertions, e.g. `assert(result != NULL)`. – M Oehm Aug 22 '15 at 20:18

4 Answers4

3

Welcome to the world of production quality code. There are some macro and factoring tricks that can make error checking less verbose. Exceptions are another mechanism. But the main adjustment to make is point of view. You are thinking of "the real work" as something separate from error handling. It's not. Dealing with all possible conditions is the essence of software engineering. To get it out of your system, explain "the main work" in comments, then write the real algorithm with externally imposed condition handling front and center.

Gene
  • 46,253
  • 4
  • 58
  • 96
0

This is the main reason for exceptions, but I have to admit I'm not a friend of introducing exceptions in a language using explicit memory management, so this answer might be biased. That said, there are a few common strategies for separating business logic from error handling in .

  1. Use exceptions -- have a look at longjmp()/setjmp() for implementing them yourself in . I would advise against it.
  2. If there is cleanup to do in case of an error, put it at the end of the function and goto there. (yes, really!)
  3. For checking a return value and printing a message to stderr, try to factor out common cases and define macros like e.g. in your case:

    #define CHECK_RETVAL(val, action) do { \
        if (val < 0) { \
            fprintf(stderr, "error calling " action ": %s\n", strerror((val))); \
            goto error_cleanup; \
        }} while (0)
    
0

If line's length is little concern to you, you can write the following function:

int checkforError(int errorCode, const char* message)
{
    if (errorCode != 0)
    {
        printf("%s: err = %d\n", err", message, errorCode);
        return 0;
    }

    return 1;
}

And use it to shrink two last ifs like this:

checkforError(err = get_number(&number), "error calling get_number") && checkforError(err = calculate_result(number, result), "error calling get_result"); 
return err;

Since the first if has little in common with other cases, there is no reason to accommodate checkforError to it.

Short circuiting has guaranteed order of evaluation, so it's not undefined behaviour. See Logical comparisons: Is left-to-right evaluation guaranteed?

Community
  • 1
  • 1
Darth Hunterix
  • 1,484
  • 5
  • 27
  • 31
0

The only situation where error handling code is unnecessary is when it's redundant. Avoiding redundant error handling code is exactly the same as avoiding redundant code in general. The only question is what the scope for the error handling code can be. Generally factor as much common behavior into as large a scope as possible.

For example, malloc failing is generally fatal so instead of checking every return value, you can wrap the function...

void* fmalloc(size_t n) {

  void* const m = malloc(n);

  if (!m)
    on_fatal("out of memory");

  return m;
}

If the behavior can only be scoped to the calling function, you can use a goto...

int on_file(fd, rm, wm) {

  if (read(fd, rm, 8) < 0)
    goto err;

  if (write(fd, wm, 8) < 0)
    goto err;

  return 0;

  err:
    on_error("on_file error");
    return -1;

}

For things that can be parameterized, parameterize them.

Here's some example code I use. In general, reducing error handling code is no different than just grouping common behavior.

Jason
  • 3,777
  • 14
  • 27