lobsters score: 3 · tags: c
I’m not going to address goto vs defer and just talk about the control flow graph structure:
Note that goto for error handling (and similar uses) maps directly to structured programming with multi-level exits. Translating the article’s example to Rust using this recipe:
'err_mem: { // open the fd if fd == -1 { break 'err_mem; } // return successfully } // free mem // return unsuccessfullyIt maps to Wasm’s structured control flow in exactly the same way: br or br_if to an enclosing block.
Anyway, this should convince you that the difference is not that the control flow is unstructured (irreducible) with goto error handling and structured (reducible) with multi-level exits. The difference is that with blocks and multi-level exits the syntactic nesting (and indentation) grows as you add multiple exit points. But you can change that by making different syntax design choices in your language.
And if you continue to use goto for C error handling (like many of us do), then you can use this to help win the argument the next time someone objects. :)
score 5 · canonical
C is getting a Zig-like defer feature https://www.open-std.org/jtc1/sc22/wg14/www/docs/n3928.pdf
Worth noting that Golang’s defer is different from the Zig/C version: in Golang defer is scoped to a function, whereas in Zig and C it is scoped to a block. If you defer in a loop, Golang will build up an unbounded amount of work to do later (allocating on the heap), but Zig and C will run each defer statement between each iteration of the loop (using constant space).
score 2 · canonical
For what it’s worth, defer ends up (IMO) not cleaning up the code that much. It got standardized partly because everyone was already inventing their own versions of it with compiler extensions, so the standards committee had to chose between the language having it in a standard form or a nonstandard form.
(I attended that standards meeting)
score 6 · canonical
defer ends up (IMO) not cleaning up the code that much
I think this is one of those cases where old guys (like me) who have been dealing with this bullshit for 30+ years don’t realise how much unnecessary cognitive load they (we) have been coping with.
The usual C resource allocation pattern (which this article describes perfectly) has several things you need to pay attention to:
-
Keep track of where important resources are allocated. NOTE: you can’t tell which local variables own important resources at the point of their declarations, but you must work it out.
-
Read the cleanup code (the goto targets) at the bottom of the function to get an idea of what resources might need cleanup. NOTE: you can’t tell which resources that are cleaned up on error paths are not temporary, and will outlive the function when it returns successfully.
-
Examine every early return (every goto fail) and compare how the position of the goto relates to resource allocation and the cleanup code. NOTE: make sure they are nested properly, and be aware of if/when cleanup is a no-op for a default initialized variable or a failed allocation.
-
Run through the happy path and make sure that temporary resources are freed (which usually duplicates the goto fail cases).
The advantage of defer is that it makes clear at the point of the defer statements which variables own temporary resources, and you no longer have to manually cross-check the success and failure cases.
I think it’s a mistake to judge defer by how clean the code looks, because cleanliness is often judged by how many lines get deleted. If you have well-written C then defer only moves code around. If you judge a change from
gotofailtodeferby its diff then you are unlikely to consider how much state you need to keep in your head when reading the code in between the allocation and the cleanup.The advantage of defer is how much less useless mental state you need to keep track of when reading the useful part of a function.
score 4 · canonical
I spent a bunch of time experimenting with this, and while defer isn’t bad, I also don’t think it makes things significantly better – it swaps one bit of cognitive overhead for another. I found myself writing a lot of accidental use after free that I’m not sure I would have with ‘goto error’, because the defer runs on all paths. You’ll probably find yourself writing a great deal of:
defer if(x != NULL) cleanup(y);
score 1 · canonical
-
