6 ms·
On the subject void foo() { for (;;) { try { return; } finally { continue; } } } is my favorite cursed Java exceptions construct.
by marginalia_nu 1y ago
On the subject
void foo() {
for (;;) {
try { return; }
finally { continue; }
}
}
is my favorite cursed Java exceptions construct.
- cerved 1y agoTo anyone wondering, I believe it's cursed because the finally continue blocks hijacks the try return, so the for loop never returns
- chii 1y agosee, if you only had GOTO's, this would be obvious what is going on!
- deleted 1y ago[deleted]
- taneq 1y agoSo the function returns, and then during its tidyup, the 'continue' basically comefrom()s the VM back into the loop? That is, indeed, cursed.
- friendzis 1y agoI would not call this snippet particularly "cursed". There is no "aktshchually this happens, therefore this is activated" hidden magic going on. The try-catch-finally construct is doing exactly what it is designed and documented to do: finally block is executed regardless of the outcome within try. The purpose of finally block is to fire regardless of exceptionality in control flow. Surprising at first? Maybe. Cursed? Wouldn't say so. It is merely unconventional use of the construct.
- Joker_vD 1y agoIt messes with the semantics of "return" statement. The conventional intuition is that right after a "return" statement completes, the current method's invocation ends and the control returns to the caller. Unfortunately, the actual semantics has to be that is "attempts to return control": The preceding descriptions say "attempts to transfer control" rather than just "transfers control" because if there are any try statements (§14.20) within the method or constructor whose try blocks or catch clauses contain the return statement, then any finally clauses of those try statements will be executed, in order, innermost to outermost, before control is transferred to the invoker of the method or constructor. Abrupt completion of a finally clause can disrupt the transfer of control initiated by a return statement. This, I believe, is the only way for "return E", after the evaluation of E completes normally, to not return the E's value to the caller. Thanks for a needless corner-case complication, I guess.
- xxs 1y agofinally stuff is executed prior to the 'return'; effectively it's placed before the 'return' statement. So it's quite obvious.
- Joker_vD 1y agoNo, it's not executed before the return, it's executed in the middle of it, so to speak: class Main { static int f() { System.out.println("f()"); return 1; } static int g() { System.out.println("g()"); return 2; } static int h() { try { return f(); } finally { return g(); } } public static void main(String[] args) { System.out.println(h()); } } will print f() g() 2 If "finally" block was executed before the "return" in the "try", the call to f() would not have been made.
- xxs 1y agoYes, very much is... return is not a single statement or a single opcode. the call to f() is returned on the stack, that's assigned to some local variable via istore, there is no 'ireturn', yet. Then the g() method is called along with whatever is printed, and the next instruction can be ireturn. which would return '2' indeed.
- Joker_vD 1y ago> return is not a single statement. It is a single statement. It's just that executing it contains of two parts: evaluating the expression, and the non-local transfer of control ― and the weird part is that this control transfer can be hijacked. And it is weird, you can't do this with e.g. "if": if the test condition is evaluated successfully, then the control will go into one of the branches, and you can't mess with that... but with "return", you can abort this transfer of control. Which is, again, weird: all other expression-evaluating statements can go haywire only if the evaluation of the expression inside them throws an exception — but if that doesn't happen, the control transfer is completely trivial, with no special rules about try-catch blocks.
- alpinisme 1y agoSufficiently unexpected (surprising) is cursed. If anything, that’s the way most people use that term for programming language constructs (weird coercions in JavaScript, conflicting naming conventions in PHP, overflow checks that get erased by the compiler due to UB in c/c++, etc.)
- friendzis 1y ago> Sufficiently unexpected (surprising) is cursed. I think there three distinct classes of surprising that fall under this umbrella: 1. a behavior overrides another behavior due to some precedence rules 2. behavior of a construct in language x is different than behavior of similar construct in most mainstream languages 3. who in their right mind would design the thing to behave like this. Examples could be: string "arithmetic" in most dynamically typed languages, PHP's ternary operator order of precedence and Python's handling of default function arguments. In all of these cases when you sit down, think about it and ask yourself "what's the alternative?", there's always a very reasonable answer to it, therefore, I think it is reasonable to call these behaviors cursed. In this case the surprise factor comes in, because we are used to equating finally block with cleanup and I concur that many would trip on this the first time. But if you gave this exercise some time and asked "what should happen if finally block contains control flow statements?" the reasonable answer should be "take precedence", because the behavior would be surprising/cursed otherwise. That's my reasoning.
- Akronymus 1y ago> In this case the surprise factor comes in, because we are used to equating finally block with cleanup and I concur that many would trip on this the first time. But if you gave this exercise some time and asked "what should happen if finally block contains control flow statements?" the reasonable answer should be "take precedence", because the behavior would be surprising/cursed otherwise. Wouldn't the reasonable behaviour be "throw a compiler error"?
- friendzis 1y agoCan the compiler perform sufficiently deep static analysis to cover all possible cases? At least in Java it is by definition impossible.
- taneq 1y agoIt's been many, many moons since I touched Java but I would have expected this to run the finally { } clause and then return from the function (similar to how in C++, objects on the stack will run their destructors after a return call before the function ends. I certainly wouldn't expect it to cancel the return.
- bear8642 1y agoThis is exceedingly nasty. Well Done!
- kfuse 1y agoThat's not just Java and there is nothing really cursed about it: throwing in a finally block is the most common example. Jump statements are no different, you can't just ignore them when they override the return or throw statements.
- metaltyphoon 1y agoIt is Java as C# disallow this
- Maxatar 1y agoIt is just Java as far as I can tell. Other languages with a finally don't allow for explicitly exiting the finally block.
- maxdamantus 1y agoAnd JavaScript .. And Python (though as sibling posts have mentioned it looks like they're intending to make a breaking change to remove it). EDIT: actually, the PEP points out that they intend for it to only be a warning in CPython, to avoid the breaking change
- o11c 1y agoNotably, C++ and similar languages don't support lexical `finally` at all, instead relying on destructors, which are a function and obviously cannot affect the control flow of their caller ... except by throwing exceptions, which is a different problem that there's no "good" solution to (during unwinding, that is).
- kmeisthax 1y agoI thought destructors were all noexcept now... or at the very least if you didn't noexcept, and then threw something, it just killed the process. Although, strictly speaking, they could have each exception also hold a reference to the prior exception that caused the excepting object to be destroyed. This forms an intrusive linked list of exceptions. Problem is, in C++ you can throw any value, so there isn't exactly any standard way for you to get the precursor exception, or any standard way for the language to tell the exception what its precursor was. In Python they could just add a field to the BaseException class that all throwables have to inherit from.
- ptx 1y agoPython has the same construct but is removing it, starting with a warning in version 3.14: https://peps.python.org/pep-0765/ https://peps.python.org/pep-0765/
- teddyh 1y agoThere was a recent talk at PYCON UK about it, by one of the authors of the PEP in question: <https://www.youtube.com/watch?v=vrVXgeD2fts https://www.youtube.com/watch?v=vrVXgeD2fts>
- ptx 1y agoThat was a great talk. I was somewhat skeptical of this change, but she really looked at it from every angle and makes a strong argument for the benefits of disallowing the construct. Also, I didn't know that (as mentioned in the talk) C# already disallows it.
- maxdamantus 1y agoInteresting .. from the post above: > The projects examined contained a total of 120,964,221 lines of Python code, and among them the script found 203 instances of control flow instructions in a finally block. Most were return, a handful were break, and none were continue. I don't really write a lot of Python, but I do write a lot of Java, and `continue` is the main control flow statement that makes sense to me within a finally block. I think it makes sense when implementing a generic transaction loop, something along the lines of: <T> T executeTransaction(Function<Transaction, T> fn) { for (int tries = 0;; tries++) { var tx = newTransaction(); try { return fn.apply(tx); } finally { if (!tx.commit()) { // TODO: potentially log number of tries, maybe include a backoff, maybe fail after a certain number continue; } } } } In these cases "swallowing" the exception is often intentional, since the exception could be due to some logic failing as a result of inconsistent reads, so the transaction should be retried. The alternative ways of writing this seem more awkward to me. Either you need to store the result (returned value or thrown exception) in one or two variables, or you need to duplicate the condition and the `continue;` behaviour. Having the retry logic within the `finally` block seems like the best way of denoting the intention to me, since the intention is to swallow the result, whether that was a return or a throw. If there are particular exceptions that should not be retried, these would need to be caught/rethrown and a boolean set to disable the condition in the `finally` block, though to me this still seems easier to reason about than the alternatives.
- ziml77 1y agoJust tested that in C# and it seems they made the smart decision to not allow shenanigans like that in a finally block: CS0157 Control cannot leave the body of a finally clause
- o11c 1y agoWhat about throwing an exception from the finally clause?
- messe 1y agoThis loops, if that's what you're asking: while (true) { try { try { return; } finally { throw new Exception(); } } catch { } }
- ziml77 1y agoYes that is the one exception (heh) to the rule unfortunately. You can throw from anywhere so it must support unwinding from any point. So if you were really intent on abusing the finally you could wrap the try-finally in a try-catch and then throw from the finally and put your continue in the catch.
- kant2002 1y agoThe finally behave slightly different in CIL. You have protected regions and finally/fault/catch/filters handlers attached. So in order to support continue inside finally you should introduce some state machine , which is complication and generally against Roslyn design limitation.
- MaxBarraclough 1y agoziml77's point is about the behaviour of the C# language. You seem to be talking about implementation concerns. They're not relevant.
- brunoborges 1y agoIn JDK 25, you can run this code: $ cat App.java void main() { for (;;) { try { return; } finally { continue; } } } $ java App.java
- scrame 1y agothis broke my head. I think I haven't touched Java in a while and kept thinking continue should be in a case/switch so ittook a minute to back out of that alleyway before I even got what was wrong with this.
- xxs 1y agotry/finally is effectively try/catch(Throwable) with copy all the code of the finally block prior to exiting the method. (Java doesn't have a direct bytecode support for 'finally') Nothing that cursed. It compiles to this: void foo() { for (;;) { try { continue; return; } catch (Throwable t) { continue; } } }
- X0Refraction 1y agoWon't that particular code fail to compile in java because the return is unreachable?
- neocron 1y agoIsn't this just an endless loop with extra steps?
- buggymcbugfix 1y agoThe cursed thing here is `continue`!