[This is probably common knowledge that are repeated in many different places, but I want to arrange it in the perspective that helps my other article to explain how this influence the idea of ContextManager in Python and why ContextManager is still a clumsy way and there is a much neater way to do tackle this classic cleanup problem]
In C, there’s no built-in exception handling. Yet the goal for cleanups is for all manually checked and handled conditions (‘exceptions’) to land exactly in the same graveyard where the resources stands a chance to be released before the program ends. This screams goto
(or longjump
which is a non-local goto that can march outside the current function) and indeed it’s the only legit use of the goto
statement I know that doesn’t make the code more error prone and confusing by littering the end of all your loops with if(error){break;}
.
With this feedback approach, all hell breaks loose if you later add another layer and forget to add this. The mistake will break the feedback chain and the code continues to run in the layer after the end of the the while loop you forgot to place the check in, which is an insidious bug if the unwanted execution are benign under most situations.
The break
clause will also become meaningless (and compiler invalid) if you convert your loops to non-loops or when you work in the top layer which is likely not inside a loop (even if you are in a bare metal embedded system with a while(0)
loop, you don’t want to break that either).
break
is a black-list approach which denies the rest of the code in the loop from running when the first error struck. Without break
, you can do the reverse (the white-list approach) and put all code after the first check in if(!error)
check blocks to authorize their execution instead
One hack to use break
statement at the top-layer (no-loop) is to wrap the top layer with a do{BLOCK}while(false);
loop which runs once, but the intention is not intuitive from the code so I wouldn’t do this to other programmers who don’t know the idiom without making a TRY-CATCH-FINALLY macro.
// Messy approach without do-while loop wrapper hack in top layer // Convention: error=0 (false) means success // The error code matches the check# it fails in this example int error = 0; if( int* f = grab_resource_and_spit_zero_if_fail() ) { // This is hell of messy if you are not in a loop // that can take advantage of break-statement // // If you don't use breaks (which require it to be in a loop), // you have to explicitly surround all code in if(!error) blocks ... // Approach 1: Nesting // Upside: Visually draws out what the logic relation is // when you're doing checks all over the place // It's hard to get it conceptually wrong // (i.e. can debug blindly with mere semantics) // Downside: more checks means more nests // you either have excessive indentation // or have fun tracking brackets if( !is_fail_1() ) { ... if( !is_fail_2() ) { ... if( !is_fail_3() ) { (you get the idea) ... } else { error=3; } } else { error=2; } } else { error=1; } // Approach 2: Linear approach // Idea: Surround everything under if(!error) check. // Error code will stick to the first error as // any non-zero error will short-circuit the // checks after it so the original error code stays // Upside: No nesting. Easy to follow to recipe consistently // WARNING: Thou shalt not be tempted to modify error code // in if(!error) blocks! // Downside: If you violate the cardinal rule above, unintended // chunks of code in if(!error) blocks might run and // it's hard to debug/discover if(!error) { ... } // this idiom is a branchless way to do // if(!error){ if(is_fail_13){error=13;} } // The error==0 should be placed in front to // take advantage of the short-circuit evaluation // to avoid actually running the check if there's // pre-existing error error = (error>0)*error + (error==0 && !is_fail_13())*13 if(!error) { ... } error = (error>0)*error + (error==0 && !is_fail_14())*14 if(!error) { ... } error = (error>0)*error + (error==0 && !is_fail_15())*15 if(!error) { ... } // Now you are in the first loop so you are allowed to use break for(...) { ... (is_fails 16 .. 41) ... if( is_fail_42() ) { error = 42; break; } .. while(...) { ... (deep down in the nest) ... (is_fails 43 .. 101) ... if( is_fail_102() ) { error = 102; break; } ... } if(error) { break; } ... } if(error) { break; } ... clean_the_f_up(f); } // goto approach if( int* f = grab_resource_and_spit_zero_if_fail() ) { ... (is_fails #1 .. 18) ... for(...) { ... (is_fails 19 .. 41) ... if( is_fail_41() ) { goto graveyard; } ... while(...) { ... (deep down in the nest) ... (is_fails 43 .. 101) ... if( is_fail_102() ) { goto graveyard; } ... } ... } ... graveyard: clean_the_f_up(f); }
By jumping to the graveyard, we don’t need to litter the code with a long chain of error message/signal feedback and/or guard all chunks of code with if(!error)
blocks, which is messy because it’s basically re-inventing a lightweight custom exception handling infrastructure that propagates the fault back to the top and give the intermediate layers a chance to intercept it.
As long as you are not using the goto
approach to do complicated maneuvers and keep it simple: all faults go to the same bucket, no ifs-and-buts or detours (i.e. no code elsewhere/in-between can intercept the flow), it isn’t spaghetii code: there are no complicated code flow graphs, just every branch pointing to the same destination in one step. You don’t need to feel guilty about using the goto
approach if your error handling flow is like this: