4

It's a good practice (I believe) in C to handle errors like this:

int status = tree_climb(tree, ...);
if (status != 0) {
    global_logger.message(2, "Cannot climb a tree %s", tree->name);
    return EPIPE;
}

or, alternatively

forest_errno = tree_climb(tree, ...);
if (forest_errno != 0) {
    goto exit;
}

exit:
if (tree) {
    tree_dispose(tree);
}
if (forest) {
    forest_dispose(forest);
}

return forest_errno;

The questions is:

What are the reasons not to use a preprocessor macro for this? I cannot remember seeing a lot of code like:

#define CHECK_OR_RETURN(contract, error_status, log_level, message_format, ...) \
if (!(contract)) { \
    global_logger.message(log_level, message_format, ##__VA_ARGS__); \
    return error_status; \
}

I see these reasons to use a macro:

  • This will collapse 4 lines of code, 3 of which do nothing during normal flow of operation, into 1.
  • Error handling, ideally, should not pollute the code for a normal flow.

I see these reasons not to use a macro:

  • Macro will obtuse reading and debugging the code somewhat. Self-invented "magic" syntax can turn a familiar language into an incomprehensible one - sometimes, you just cannot parse a code without knowing a library specifics. Though, on a large scale, a code 3 times shorter is 9 times more readable.
  • In C++, we could use exceptions. I would rather not use non-checked exceptions, but it's not in the scope of my question.
  • Some people just don't care for the code length.

So why don't people do this all the time?

Victor Sergienko
  • 361
  • 1
  • 2
  • 13

3 Answers3

5

Your "alternative" shows that your C code might implement a stepwise acquisition of resources.

Take the following example, without any error processing, and indentation just used here to show the different steps:

 FILE *fp = fopen("data.txt", "rb");          // step 1: acquire file resource
   Forest* forest = generate_forest(fp,...);    // step 2: allocate tree resource 
     Tree *tree = get_tree(forest, "SPAIN"...);   
     Element *element = malloc(sizeof(element));  // step 3: allocate new element
       element->value = MAGIC; 
       tree_add(tree, element);                 // suppose element is copied into the tree
     free (element);                          // end step 3 
     ...
   release_forest (forest);                 // end step 2 (forest takes care of its trees)
   ...
 fclose (fp);                             // end step 1

There can be an error caused by each single instruction here. Depending on the step in which your error, happens, you might have to release some or some more resources.

Alternative 1

Using your macro to log and process the different errors would just leak resources, by returning whle not all resources are released.

Alternative 2

Using some nested conditional statements:

 FILE *fp = fopen("data.txt", "rb");          // step 1: acquire file resource
 if (fp) {                                    // go on for step 1
     Forest* forest = generate_forest(fp,...);    // step 2: allocate tree resource 
     if (forest) {                                // go on for step 2
        Tree *tree = get_tree(forest, "SPAIN"...);   
        ...
        release_forest (forest);                  // end step 2 (forest takes  
     } 
     else {
        errcode = logmyerror ("forest not properly loaded", ENOTFOUND);
     }
     fclose (fp);                             // end step 1
 }
 else {
     errcode = logmyerror ("file not found ", EOOPSAGAIN);
 }
 return errcode;   // final exit point 

Alternative 3

The ignomous goto (well it has some advantages as you'll see), where different labels correspond to exit points for the relevant steps:

 FILE *fp = fopen("data.txt", "rb");          // step 1: acquire file resource
 if (fp==NULL) {
     errcode = logmyerror ("file not found ", EOOPSAGAIN);
     goto out0; 
 }
 Forest* forest = generate_forest(fp,...);    // step 2: allocate tree resource 
 if (forest==NULL) { 
     errcode = logmyerror ("forest not properly loaded", ENOTFOUND);
     goto out1; 
 }  
 Tree *tree = get_tree(forest, "SPAIN"...);   
 ... 
out2: 
 release_forest (forest);                 // end step 2 (forest takes care of its trees)
   ...
out1: 
 fclose (fp);                             // end step 1

out0: 
 return errcode; 

Conclusion

Of course if you don't have such multilevel resource allocation, you could perfectly use your macro. But I'm afraid that there are many real life situation where this will not be sufficient.

Note that in C++ you would not need this if you use RAII, the destructors would always clean the resources whenever you return.

Christophe
  • 74,672
  • 10
  • 115
  • 187
  • I don't see what you mean, can you give a counterexample please? Now I believe that: 1. 99% of the time, resource deallocation can be done in 1 block, like in the snippet 2, - it will be just `goto exit;`. 2. A sequence of `return`s is algorithmically equivalent to a pyramid of nested `if`s. And nesting level > 4 is a code smell. – Victor Sergienko Nov 11 '16 at 18:42
  • @VictorSergienko I've completed my answer to be more illustrative about the problems I tried to depict. Hope this doesn't get too long :-) – Christophe Nov 11 '16 at 19:35
  • Thank you very much for your effort. I'm not convinced still. The 2nd example deallocates the resources correctly, no matter what step broke, with a single label. I elaborated here: https://gist.github.com/singalen/4321e0cc7acaddcb9673d6d509b456ec taking your example as a base, without macros. Of course, the macro will have to have `goto exit` instead of `return` - This imposes extra requirements on the hosting function, of course, and reduces the readability. BTW I'm not asking why don't people use EXACTLY this macro, but it's a sample of technique. – Victor Sergienko Nov 11 '16 at 22:20
2

People don't do for the reasons you listed. Additionally there is no common "good coding praxis", but in the best case you have some departmental rules. Also I find lots of code which is not coherent. That is, parts use the macros and parts don't. Depending likely on which programmer had hands on.

So which advise can I give? If you have lots of those simple error handling snippets feel free to use a macro. I think that dense code is better to read than scattered, especially with those repetitive snippets.

Of course, if you have more complex error handling, you might end up with unmanageable macros.

0

If you are missing exceptions in C, you may emulate them with "long jumps". This way, you can have one point for handling the exceptions, like with try/catch, and this point can even be on a higher stack level.

Here is an example implementation, but the basis (longjmp / setjmp) was part of C from the beginning, and there is no additional software necessary. http://www.di.unipi.it/~nids/docs/longjump_try_trow_catch.html

rplantiko
  • 119
  • 3
  • 1
    Thanks! I do not miss exceptions in the slightest. Even Java checked exceptions are very easy to misuse. `longjmp` emulation not checked by the compiler and adds preprocessor on top. But it's a very good piece of craft anyway! – Victor Sergienko Nov 14 '16 at 17:45
  • "Adding preprocessor on top" is not necessary, it was only that the mentioned solution used macros. I use the concept with reluctance, although it does improve readability when compared with the alternative: to drag return codes through a bunch of stack levels, always followed by the same `if (rc ==... ) { ... }` boilerplate code. – rplantiko Nov 15 '16 at 17:56