Tags

, , , ,

Does ‘return’ always cause a function to return? Surprisingly the answer is “no”. Indeed there are situations in which ‘break’ may not always break from a loop, and ‘goto’ may no go anywhere. I discovered this corner of C++ while writing my own compiler for Leaf. Before you brand it a C++ problem, I had no difficulty recreating the same scenario in Java and Python (see bottom of this blog entry for analogous examples). It’s likely that any language with destructors, finally, or defer clauses is affected.

What?

What do you think the below code outputs?

#include <iostream>
using namespace std;

struct fail {
	~fail() {
		throw 1;
	}
};

int main() {
	for( int i=0; i < 6; ++i ) {
		cout << "Loop: " << i << endl;

		try {
			if( i == 3 ) {
				fail f;
				return 0;
			}
		} catch( int ) {
			cout << "Caught" << endl;
		}
	}
	return 0;
}

If you think the loop stops after ‘i == 3′, you’re wrong. The destructor of ‘f’, which executes just before the ‘if’ scope ends, throws an exception. This exception is caught by the ‘catch(int)’ handler, which interrupts the return flow and resumes normal iteration over the loop. It has forgotten that it was actually returning from the function.

Should this happen?

To me this feels like a defect in the language. I’ve looked at the C++ standard and could not find decisive wording that would indicate this should happen. When I posed the question elsewhere, I was essentially told that the exception flow is dominant. That is, when an exception is thrown, it becomes the sole flow; thus a handler will catch that exception and forget about the return flow.

I don’t like this explanation for two reasons:

First, I don’t find the wording of the standard to be explicit about what happens. Section 6 and 15 of the standard, dealing with flow and exceptions, both generally ignore that a combination of flows might be happening. While they clearly state an exception introduces a new flow, they use the same strength of wording to indicate ‘return’ and ‘break’ introduce new flow. There is no text indicating that a ‘return’ or ‘break’ is a “conditional” flow. Indeed their wording on ‘goto’ seems much stronger (“unconditionally transfers”). Yet ‘goto’ is also interrupted by an exception.

Second, diverted flow is already a part of the language. When you issue a ‘return’, the code must flow through any destructors and defer statements. At the end of those deferred blocks, the return flow continues: it is not lost due to the deferred statement. This means there is nothing fundamentally different about exception flow.

To learn more about flow refer to my article on basic blocks.

Why?

Why this happens is likely a result of how compilers implement exception handling. Indeed, when I implemented exception handling in Leaf, the natural consequence was this interrupted flow. However, I may have implemented the diverted flow technique first and allowed the return flow to continue.

Unfortunately you’d have to look closely at basic block flow and have a fairly deep understanding of the compiler to see exactly why these other languages interrupt the return flow. It is without a doubt the easier approach, but allowing the ‘return’ to continue is certainly not that difficult.

What should happen?

Now, relying on this behaviour, or even having exceptions within defer statements, is a sign of of some bad code. This is perhaps why the issue doesn’t come up much. In C++ you just have to live with a very limited exception handling system. If you play by the best practices, you shouldn’t end up in this situation. It is also not clear whether the ‘return’ should necessarily continue. Perhaps the programmer does intend the exception handling to be the dominant flow. From a programmer’s perspective I’d say it looks ambiguous.

Ultimately my basic opinion is that ‘return’ should return from a function, as should ‘break’ break from a loop. These are not conditional statements. In Leaf I intend to disallow these situations with a compiler error to remove the ambiguity. I can’t think of an example of a good coding practice which is disrupted by this limitation. What do you think?

Other Examples

Java

package investigate;

class Flow {

	static public void main( String[] args ) {
		for( int i = 0; i < 6; ++i ) {
			System.out.println( "Loop: " + i );

			try {
				try {
					if( i == 3 )
						break;
				} finally {
					if( i % 2 != 0 )
						throw new Exception();
				}
			} catch( Exception e ) {
				System.out.println( "Caught" );
			}
		}
	}
}

Python

for i in range(6):
	print("Loop:", i)

	try:
		try:
			if i == 3:
				break
		finally:
			if i % 2 != 0:
				raise Error()
	except:
		print("Caught")
About these ads