Philosophy

Never use the “continue” keyword!

I was looking through some questions on StackOverflow last week when I came across a curious answer. In it there was a link to a coding standard that forbade the use of the “continue” keyword. There was even positive support for this answer. Needless to say, I was aghast. It’s like the arguments against “goto” have run amok and started targeting other language keywords.

Naturally I can’t just condemn such principles without showing why it is a problem. Looking through my own code it is certainly a commonly used keyword. While alternatives do exist, “continue” is an essential aspect of flow control which helps to reduce code and improve clarity. Instead of trying to prove its worth, I’ll instead go the other direction and show the consequences of not using “continue”.

Reductio ad Falsam

Avoiding the minor arguments, let’s jump right to a logical hypothesis: forbidding “continue” also requires forbidding any non-tail “return” statement. That is, if you believe using “continue” is wrong, logically our argument must also forbid the use of using a “return” statement anywhere other than as the last statement of a function. They both prematurely interrupt the flow of execution for a particular block of code.

We can show this using some simple code examples. First let’s start with a very typical loop using the the “continue” keyword.

for( int i : range )
{
    if( some_cond( i ) )
		continue;

	...
}

Now let’s use a function instead of the inline loop body. I have chosen this approach since as it is a common recommendation.

void process( int i, ... )
{
	if( some_cond( i ) )
		return;

	...
}

for( int i : range )
	process( i, ... );

By doing this you have not modified the meaning of the code at all. An optimizing compiler could very well compile both code bits exactly the same. You have however increased the size of the code, and moved the loop body logic away from the loop — some people may find this lowers readability. Take this a step further in a language that supports closures (I’ll attempt to use a C++11 lambda):

Ignoring some syntactic bloat, this looks like the original code sample, albeit without a “continue” statement. By using a closure we can meet the requirement yet still produce the same code.

for( int i : range )
	[](int i) {
		if( some_cond( i ) )
			return;

		...
	} (i);

These code samples are to illustrate that using a non-tail “return” can be identical in meaning to using a “continue” statement. This would imply that whatever reason one would have to forbid the use of “continue” would also apply to this use of the “return” keyword. My intuition is that if I took this line of reasoning far enough the omission of “continue” might actually require a pure functional programming approach. Simply forbidding the use of a non-tail “return” is likely enough to show the absurdity of the original requirement.

Practical Example

By way of example let me show how not allowing “continue” will produce what I consider to be bad code. Looking through my recent project I found a sample that looks somewhat like the below (it scans a list of logical objects looking for those which can be processed).

for( object_id_t id : proc_list )
{
	object * obj = find_object( id );
	if( !obj )
		continue;

	if( obj->is_active() )
		continue;
		
	time_t elapsed = now() - obj->begin;
	if( elapsed < timeout )
		continue;

	...
}

If we forbid “continue” (and non-tail “return”) this code would have to be convereted using embedded “if” blocks.

for( object_id_t id : proc_list )
{
	object * obj = find_object( id );
	if( obj )
	{
		if( !obj->is_active() )
		{
			time_t elapsed = now() - obj->begin;
			if( elapsed >= timeout )
			{
				...
			}
		}
	}
}

High levels of block depth are difficult to read. It is difficult to visually identify where the block ends and what the chain of conditions is. Converting this to a series of functions would result in a lot of nearly trivial functions. A large number of trivial forwarding functions are also hard to read as it is difficult to follow the code. I find the first code example, using “continue”, to be a very clear way to write the code.

Conclusion

Often requirements get taken out of context, and this might very well be one of those situations. The StackOverflow answer picked one aspect out of a very large document. Out of context, and without rationale I definitely see the requirement (of not using “continue”) to be more harmful than beneficial. When I looked through the requirements, most of them actually have a “Rationale” entry. This is very good practice; you should always document why you have certain restrictions. Strangely however the requirement on “continue” was lacking such a rationale, so we have no idea why it is not allowed — and in particular why they still allow non-tail “return” statements.

10 replies »

  1. Hi Edaqa,
    (I am not sure if this is what you wish to be called, but this is what the site says). Thanks for the article. It really makes me reflect about the style of programming. I also like your approach of challenging the “dogmata”.
    I personally sympathize with the opponents of ‘continue’, and I want to challenge your argumentation. It is not that I never want to use it, but there is something too “low-level” about it. It is essential that you used the new for-each loop in your examples. This loop is used to express the intention that some operation needs to be performed for every element in a collection. But then, using continue appears to contradict that: you say that, in fact, you do not want the operation to be performed for some of the elements. Effectively by using continue you are doing a “for-some” loop. But if you really do a “for-some” loop you could be more explicit about it than just putting continue statements.

    In my work we use a sort of range as in Boost.Range: that is, an iterator that is aware when it is done iterating – it does not need the one-past-end iterator to tell when it reached the end. With such range it is easy to define a filtered range, given a predicate pred we can use some function (in our case opertor|) to build a filtered range view:

    for (auto e : range | pred) {...} 
    

    This makes it explicit that we want to perform some operation for every element in a filtered range. We separate the filtering logic from the operation. Under this philosophy, your last example could be written as:

    auto processible = [&]( object_id_t id ) -> bool 
    {
      if (object * obj = find_object(id)) {
        if (obj->is_active()) {
          if (now() - obj->begin < timeout) {
            return true;
          }
        }
      }
      return false;
    };
    
    for (object_id_t id : proc_list | processible) {
      ... // no continue
    }
    

    You may find the way I structured the body of function processible unattractive, you could rewrite it to have four return statements, but my message is still intact: you can separate the operation from the way you filter out elements of interest. The code is not shorter; on the contrary, I omitted the difficult implementation of operator|, it compiles slower, and may incur run-time cost, but it appears (to me) more high-level: we have a sequence, a filter, an operation, and we glue them together.

    And while I agree with you that multiple return types are similar to using ‘continue’, I do not necessarily agree that objecting to ‘continue’ should imply objections to multiple returns.

  2. I agree that a filtered approach would be clearer in many cases. Certainly I’m a strong proponent of providing better set operations in a language. C++ definitely fails in this regards. I’m guessing that writing your filter operator was painful, and likely still isn’t as efficient as the inline filtering. (I do actually disagree with changing the meaning of operators as well, but that’s a whole other issue, especially since C++ has so few symbols available).

    That aside, the filter in this approach has a significant problem, granted one that can’t be readily seen from the example. The “obj” and “elapsed” variable are no longer available to the loop body. I think this problem is fairly language agnostic, and I don’t see an easy way to fix it with the filter approach.

    Another minor issue is one of access. In this case the “find_object” function is a private function to the object doing the processing — meaning a free-standing function could not work. So without a closure you’d be force to add a new function to the class (probably in some header file), which increases the maintenance cost of the filter. This problem is highly language specific, with a better syntax (ex. a closure) this would be not a significant concern.

  3. Range based programming actually offers some solutions here, including for the accessibility of the loop variables. The problems above come from not taking the paradigm far enough. Consider the following: (typed but not compiled)

    typedef std::pair object_pair_t;

    auto object_with_id = [&](object_id_t id)
    { return std::make_pair(find_object(id), id); }

    auto is_desirable = [&](object_pair_t const& the_pair)
    { return the_pair.first != 0 && the_pair.first->is_active(); }

    auto time_has_elapsed = [&](object_pair_t const& the_pair)
    { return now() – the_pair.first->begin < timeout; }

    proc_list
    | transform(object_with_id)
    | filter(is_desirable)
    | filter(time_has_elapsed)
    | apply([](object_pair_t& object_info) {
    // …
    });

    Notice that the current form of the loop variable is passed onward with each step. This addresses the accessibility issue, with overhead at the scale of copying references. The for-loop is hidden behind the apply() call, making

    The only problem I have at this point, is that the library I am referencing needs a typedef of return_type for object_with_id's lambda, which the language does not currently offer. This could be quite easily changed to a functor, though, simply with less fortunate syntax. If you would like to see how the range library is implemented, you can check it out at:
    https://www.assembla.com/code/rob_douglas_sandbox/subversion/nodes/trunk/ranges

    "continue" is a useful keyword, when stuck with large complex for-loops, but range-based-programming (specifically range adaptors) mitigates the need for such hefty constructs, and can even encourage better, more testable, code, with clearer code paths.

    • While I agree such an approach could remove the need for goto, I’m not sure this is an improvement in code clarity. In fact I find this to be a step backwards in clarity. It takes me a while to understand exactly what is happening in your example, whereas in the example using “continue” what is happening is far more obvious. Perhaps with a proper native syntax it could be cleaner.

  4. “I was looking through some questions on StackOverflow last week when I came across a curious answer.”

    You’re missing a link.

    • I wasn’t really looking to draw attention to the answer/link, since other than spur this blog entry they aren’t too much related to it.

  5. Just a quick note that in your example the nested if’s go away if you use short-circuit evaluation in languages that support it.
    The condition would be pretty hairy but the nesting would be consistent with the problem I think, as there is really only 1 thing you want to check : whether the object is processable, for some definition of processable.

  6. Totally with ya on favoring ‘continue’ and ‘break’ and early ‘return’ when it makes the code easier to read!

    That said, there is a view that it should be possible to construct a simple graph of the algorithm’s control flows, and one feature of that graph is single entry and exit points. That’s a key reason for the prohibition regarding early return statements.

    That bleeds over into ‘continue’ and ‘break’ in that, if you view a loop as a closure, then ‘continue’ and ‘break’ are early exits of that closure. (That becomes explicit if you implement the loop as callable code.)

    Bottom line, the main objection is based on theoretical analysis of code correctness. Code where the flow of control doesn’t jump around is easier to prove correct. But analyzing the correctness of code has proven a tougher nut to crack, which makes the goal a bit pointless. I almost entirely ignore prohibitions against ‘continue’ and ‘break’ and early ‘return’ statements.

    • Anything other than trivial code can’t have simple flow. If a language offers anything like exceptions, destructors, or deferrals the flow gets really complicated. Similarly, allowing threads and external events (signals, interrupts, callbacks) makes flow even harder.

      I tried to cover the idea of flow in: https://mortoray.com/2013/05/06/building-blocks-compiling-if-defer-break-continue-and-exceptions/

      Those diagrams show why the only way to get a simple code path is to prohibit any kind of flow control beyond if-else statements and trailing return. But still, since the IR lowering phase does whatever it wants with your code, there’s no guarantee you retain the same flow at runtime.

      I’m not sure I see value in “proven” code. Unless the entire technology stack is proven, the hardware, the OS, the compiler, all libraries, and then finally your code, there is no way to ensure your proven status holds at runtime. Perhaps it is helpful for showing algorithm correctness, but there’s many ways to do that. I think the practical computer is for all intents and purpose a non-deterministic machine now. The complexity, combined with external events and timings, all but guarantees your code will be executed somewhat differently each time. I need mechanisms and theory that deal with that.

    • Proving algorithm correctness is of interest in computer science, and — quite to the contrary — it’s a non-trivial task. As you suggest, any significant code block has multiple paths of execution. Each branch — whatever its source — increases the number of paths, which is why the goal is reducing branches as much as possible.

      Systems that need to demonstrate correctness do have to go to great lengths: reducing and strictly controlling what can be installed, hardware and software checks, validation suites, etc.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s