Monday, June 19, 2017

Go To Statement Considered Helpful (part 3: Finding Matches in a Loop)

Another time I often find use for a goto is in a for loop that is scanning an array to find a match. If a match is found, a break terminates the loop. See the standard version of this below. It repeats the condition check for the loop to see if the loop terminated early. The other option would be to use a flag to indicate if a match was found. This requires initializing the variable and then setting it if a match is found. Both of these variations are very common.


for (i=0; i<ARRAY_SIZE(data); ++i)
{
    if ( data[i].key == value ) break;
}
if ( i<ARRAY_SIZE(data) )
{
    // data[i].key is our match
    match_found:
    ...
}
else
{
    // No match was found
    ...
}

I hate repeating code; that's a source of bugs, as years later someone might need to make a change and only change one of the two copies. Adding a tracking variable is clunky. Again, goto can save the day. Consider the code below:


for (i=0; i<ARRAY_SIZE(data); ++i)
{
    if ( data[i].key == value ) goto match_found;
}
if ( 0 )
{
    // data[i].key is our match
    match_found:
    ...
}
else
{
    // No match was found
    ...
}

This eliminates the repeated comparison, so if something changes, it only has to change once. A quick reaction that many will have is that this also will be faster than the alternative, but generally compilers are very good at eliminating redundant comparisons, and processors are fast enough that it's rarely worth worrying about saving a few machine instructions; in fact, it's usually best to be willing to sacrifice machine instructions for better code.

Without the goto, you can't scope the loop index to be just the loop, or you end up using an extra boolean tracking variable. Most importantly, without the goto, the code is longer and more complicated, and that's exactly what you don't want. Of course, in most cases, you'll still need the loop index to know which item matched, but sometimes you only need to know if there was a match.

Once again, the C preprocessor comes to the rescue with a clever pair of macros below:


#define EXIT_LOOP_WITH_MATCH(name) goto _loop_match_ ## _name;
#define IF_LOOP_EXITED_WITH_MATCH(_name) if(0) _loop_match_ ## _name:

The macro works just like a regular if statement. You can use it with or without braces, and you can use a regular else condition. The only catch is that the compiler will likely complain about an unused label if you have the IF macro without a matching EXIT macro. That may well be more of a benefit than a restriction.

With the macro, our previous example is nice and clean as seen below:


for (i=0; i<ARRAY_SIZE(data); ++i)
{
    if ( data[i].key == value ) EXIT_LOOP_WITH_MATCH(my_loop);
}
IF_LOOP_EXITED_WITH_MATCH(my_loop)
{
    // data[i].key is our match
    ...
}
else
{
    // No match was found
    ...
}

Those macros may look familiar. They should. They're exactly the same as the THROW and CATCH macros I discussed previously for exception handling in C.

It is interesting to note that this and the previous example for the use of goto (breaking out of a nested loop and avoiding extra code to handle exiting a loop early) are exactly the two cases cited in Kernighan and Ritchie's The C Programming Language book when it discusses the goto statement in section 3.8.

No comments:

Post a Comment