Defective Language

How to catch a “return” statement

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")

17 replies »

  1. Your approach appears to be the correct one: report a compile-time error. But can you do that in every case?

    To clarify, in C++ (with the recently added noexcept), the code you showed causes a call to std::terminate. Destructors are by default marked as “noexcept” and are not allowed to throw: but it is checked only at run-time. So C++ appears to also have taken the direction you did.

    Regards,
    &rzej

    • I’m uncertain now whether I can actually detect all such cases. It looks like I can, but high-level features like closures may make this difficult.

      Also note that a “noexcept” only works for destructors. For this to solve the problem it would also means that finally/defer statements must also be “noexcept”. I actually don’t like this idea because it further isolates the uses for exceptions (you can’t freely use them, minimizing their value).

  2. Java and Python don’t have dtors, so it shouldn’t be a surprise this isn’t an issue for them. Exception safety its known for being a challenge, there are even a few GOTW entries on it. It is fairly well known that youshould never throw in a dtor.

    • I do however show that Java and Python have the same problem. All you need is any kind of deferred statement, a destructor, a finally clause, or a defer statement. The logic of not throwing in a destructor would then need to further continue into all of these deferred statements. While that seems like an option I’m not sure it is a good option. Clearly one has to handle errors in destructors/finally/defer, and since I think a unified error model is preferred, it means there cannot be this restriction on where errors can be raised.

    • Though Java do not have dtors, it has (much like C#) the option to implement a Finalize method, which often acts like a dtor but can be much more unpredictable at times. No surprise the behaviour is easily reproduced for them.

  3. I’d like more infrmation about the python example.. pethaps this would need to use a try/except/finally/else construct? Since I believe else only tries to execute in an un’broken’ scenario

    • The Python example, as well as the others, are not meant to serve as examples of good, or even sane coding. They are just there to demonstrate this odd program flow is actually possible. In all examples there is certainly a better coding style which would avoid the problem.

  4. The behaviour you get is the behaviour one would expect. It doesn’t appear to be a ‘problem’ at all.
    The order is which ctors, statements, & dtors are executed within one’s scope has always been cristal clear for single thread. It is one of the reasons throwing exceptions in dtors is strongly discouraged. So you don’t mess with get-out-of-scope statements.

    • I disagree that people expect this behaviour — that is, I think they would understand the behaviour from the example, but this isn’t the real problem. The real problem is phrased as the question “When can a return statement not return from a function?” I doubt that most programmers would be able to answer that, and indeed many would likely argue it is not possible. The problem is that “break”, “continue”, “return”, and even “goto” are all described as uncoditional changes of flow in the standard. Nobody expects them to do something other than described. I indeed find it troubling they’re even allowed to do something else.

    • I think this is exactly same behavior as `foo((throw “Holy S****!”,5));`. calling function is also “unconditional change of flow”.

      Another thing is what we will do with that exception if it cant override `return`? Ignoring it or rethrow it in new destination? only thing that left is terminate but this can be too harsh.

      I think best solution is specify this behavior in C++14 or C++17, that we could depend on it.

  5. I have to admit my first reaction to the code was “it’ll throw an error and keep going, because you’re catching inside the iteration”. The throw/catch convention is that any code after a throw cannot be reached (most emphatically, in fact) because doing so would cause your program to continue as if it is in a consistent state, which is no longer the case. Even if the next statement is a return. Or rather, especially if the next statement is a return. To do what you want to do, rather than what you’ve coded, the return should simply come after your try/catch; if you don’t care about the failure causing an inconsistent state (or you know the state is still consistent) then you can simply return regardless of whether “fail f” succeeds or fails. But you generally don’t write code for which the result will not affect program state in a way that makes it throw exceptions that must be handled. That’s simply mixing incompatible paradigms =)

    • In the c++ example, ‘return’ is the current statement, not the next one. It is the statement that the program is trying to execute when the destructor generates an exception. Clearly the code itself is really bad, and this problem can be prevented with sane programming. Yo me the real question is: Is it okay for the program to behave in such an erratic way since that’s what the code is like, or should the compiler flag this code as an error?

  6. For my part, the C++ behaviour seems entirely correct. In the presence of exceptions and RAII, it must be expected that at any scope exit objects will be destroyed and any exceptions raised in the process will interrupt normal flow-of-execution, just as any other exception does.

    • Part of my question was whether the standard says it should work this way. I still maintain the standard itself is ambiguous, though common implementations, and perhaps even intent, do follow one single direction.

      My real problem with this is that it contributes to the difficulty in using exceptions. You simply aren’t safe using exceptions in all contexts in C++. In some cases the behaviour is undefined, and in other cases (like this), the behaviour is not always what you want.


  7. I disagree that people expect this behaviour — that is, I think they would understand the behaviour from the example, but this isn’t the real problem. The real problem is phrased as the question “When can a return statement not return from a function?” I doubt that most programmers would be able to answer that, and indeed many would likely argue it is not possible.

    I disagree with this. It’s intuitive that a return that doesn’t get hit by control flow doesn’t return (and as such, it astounds me every time I see the if return else return pattern – you know by virtue of getting to the second return that the if didn’t execute, so the else is superfluous at best and a waste of time at worst).

    It’s also intuitive that any line that can throw exceptions can/will interrupt the control flow because that’s how exceptions work – and as a bonus, that’s how it’s lexically implied in the code. If you wanted the return regardless of the exception state of fail, you should structure it differently. You put the return in the catch clause or after the entire try block and the exception doesn’t propagate up the stack, it’s obvious that control flow will get to your return and that it will be executed.

    You might be right that C++ standard is underspecified, but to me, this is the obvious and intuitive behavior. Additionally, your conclusion that exceptions are difficult to use is correct in general and they should be used sparingly. But I can’t agree with the base conclusion that return doesn’t return in some cases. return returns as long as it’s hit and by saying it doesn’t return in certain cases implies something entirely different about how the code works.

    • Or I could be a complete dolt and not realize the implication of the example.

      It’s definitely a bit strange that the return can execute causing an implicit teardown that throws the exception, but I still don’t think the way to model that is return not returning. To me, it still seems that the issue is the implicit cleanup behavior. Return can’t return until it cleans up all appropriate scopes so it’s not actually getting executed until that cleanup occurs.

      It’s definitely less cut and dry than I suspected at first. Suppose that’s a chalk mark for speaking before I think.

    • It definitely has started returning at that point. If you put the return in an if statement and write something after that if, you’ll see the return is indeed executing — not just scope teardown. It’s a bit hard to really see that the “return” aspect has really started. I know from Leaf compiler that it has genuinely started at this point. I can’t say for sure what other compilers have generated here, but it is likely similar since that is how I discovered the issue.

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