> If we flew planes like we write code, we’d have daily crashes, of course, but beyond that, the response to every plane crash would be: “only a bad pilot blames their plane!”
> This doesn’t happen in aviation, because in aviation we have decided, correctly, that human error is an intrinsic and inseparable part of human activity. And so we have built concentric layers of mechanical checks and balances around pilots, to take on part of the load of flying. Because humans are tired, they are burned out, they have limited focus, limited working memory, they are traumatized by writing executable YAML, etc.
> Mechanical processes are independent of the skill of the programmer. Mechanical processes scale, unlike berating people to simply write fewer bugs.
It gives me solace to know that I am not alone.
- the language still allows you write the unsafe version even with defer. By your logic fallible humans will continue to write these class of bugs because they can. - adding a whole new flow control construct will introduce a whole new class of bugs. The dog barking example is cool for demonstrating how defer works, but is completely unreadable for what it does, programmers will write code like that because they are allowed to, and unreadable code becomes buggy code. - to make a language safer you should remove the things that make unsafe behavior possible, not add constructs which make safe behavior easier.
Maybe I'm putting too much emphasis on "completely unreadable" rather than the rest of the quotation, but I find the example crystal clear, and I'd never expect code intended to illustrate, clearly and loudly, language features to read naturally.
> to make a language safer you should remove the things that make unsafe behavior possible, not add constructs which make safe behavior easier.
Some of this guy's other (equally superb) blog posts explain why this isn't an option: it breaks decades' worth of C code, and the C standards group is strongly committed to ensuring that C that compiled 20, 30, 40 years ago continues to compile.
Regardless, I find it incredibly weird to read the statement "you should [...] not add constructs which make safe behavior easier," no matter the contents of "[...]." If your goal is to improve the security of a programming language or the maintainability of code in that language, and you don't want breaking changes, this isn't just your best option. It's your only option, I think.
The only reasonable way for defer to behave. Function scoped never made sense to me given the wasted potential. The demonstration with loop and mutex being a good one.
"the defer call is hoisted to the outside of the for loop in func work"
Astonishing. Add that to the list of golang head scratchers. That is one of the biggest "principle of least astonishment" violations I've ever seen.Disclaimer: Not a golang hater. Great language. Used it myself on occasion, although I remain a golang neophyte. Put away the sharp objects.
I think per scope-level is probably better, but honestly still - as a I mention elsewhere - still something that seems fairly limited compared to writing code inside blocks that clean themselves up in the Ruby world. The more we're messing with scope, the more it seems like it would be possible to go all the way to that? The go-style defer appears likely to be simpler from an implementation POV; if we're gonna make it harder let's go all the way!
I know a lot of people hate the nesting of indentation from that, but it makes so many other things harder to screw up.
defer x // scope scoped
defer fn x // function scoped
Also: var a = 0
fn var a = 0
for fn i := …
But we have this allergy to full control and invent these increasingly stupid ways to stay alert and get unpleasantly surprised anyway.Edit: Same for iifes. Everyone uses them, so turn these into proper embedded blocks!
I think you can, for example this way:
- declare a function pointer variable cleanup - initialize it with no_op - call defer ‘call cleanup’ - if, inside a block, you realize that you want to do something at cleanup, set cleanup to another function
That’s more code, but how frequent is it that one wants to do that?
One thing this doesn’t support is having a call into third party code defer cleanup to function exit time. Does golang supports that?
Of course you can, using ?: (or && and || if you prefer), just like any other case where you want an expression rather than a statement. Or simply using the non-block form of if. (Some stupid autoformatters or tech leads insert extraneous braces, but you should be avoiding those already).
Glad to see C is evolving and standardizing.
At least this was the case last I did benchmarks of my Go code. Dno if they changed that.
https://github.com/golang/proposal/blob/master/design/34481-...
func foo() (retErr error) {
f, err := os.Create("out.txt")
if err != nil {
return fmt.Errorf("error opening file: %w", err)
}
defer func() {
err := f.Close()
if err != nil && retErr == nil {
retErr = fmt.Errorf("error closing file: %w", err)
}
}()
_, err = f.Write([]byte("hello world!"))
return err
} void* p1 = malloc(...);
if (!p1) goto err1;
void* p2 = malloc(...);
if (!p2) goto err2;
void* p3 = malloc(...);
if (!p3) goto err3;
return {p1, p2, p3};
err3: free(p2);
err2: free(p1);
err1: return NULL;
With defer I think I would have to use a "success" boolean like this: bool success = false;
void* p1 = malloc(...);
if (!p1) return NULL;
defer { if (!success) free(p1) }
void* p2 = malloc(...);
if (!p2) return NULL;
defer { if (!success) free(p2) }
void* p3 = malloc(...);
if (!p3) return NULL;
defer { if (!success) free(p3) }
success = true;
return {p1, p2, p3};
I'm not sure if this has really improved things. I do see the use-case for locks and functions that allocate/free together though. void* p1 = malloc();
if (!p1) return failure;
defer { free(p1); }
...
someOther->pointer = p1;
p1 = NULL;
return success;I initialise all pointers to NULL at the top of the function and use `goto cleanup`, which cleans up everything that is not being returned ... because `free(some_ptr)` where `some_ptr` is NULL is perfectly legal.
void* p1 = malloc(...);
void* p2 = malloc(...);
void* p3 = malloc(...);
if(p1 && p2 && p3)
return {p1, p2, p3};
free(p3);
free(p2);
free(p1);
return NULL;That means you won't forget to call it, and the success flag is an obvious way to ha dle it
Why not make the string literals in the code identify their positions in the output, to expose the behavior, rather than obfuscate it?
Then the reader only has to work through the code, to see why it would have that order.
It currently looks like a puzzle intended to be harder for the reader to understand than it needs to be.
[1] Because defer takes a block, not a simple statement. And deferred blocks can be defined recursively--i.e. defer within a defer block.
Not just GCC, but you're right it's tailored, to the same "unwinding" queue that C++ destructor, stack-VLA de-allocation and __attribute__((cleanup)) shared, won't fit into the current state of language otherwise.
Clang share more frontend between C and C++ so I imagine they can implement it as hidden C++ lambda scope-guards, the nested scenario is just full-capturing lambdas inside another.
Go's "defer" is reasonably clean because Go is garbage-collected. So you don't have to worry about something being deleted before a queued "defer" runs. That's well-behaved. This is going to be full of ugly, non-obvious problems.
Interestingly, it's not really "defer" in the Go sense. It's "finally", in the try/finally sense of C++, using Go-type "defer" syntax". This mostly matters for scope and ownership issues. If you want to close a file in a defer, and the file is a local variable, you have to be sure that the close precedes the end of block de-allocation. Most of the discussion in the article revolves around how to make such problems behave halfway decently.
"defer" happens invisibly, in the background. That's contrary to the basic simplicity of C, where almost nothing happens invisibly.
What sense is that? C++ doesn't have finally and the article explicitly calls out how its not like destructors.
defer always executes on scope exit, errdefer executes on an error exit. In principle, this is similar to the logic of a try/catch/finally:
try {
// whatever
} catch {
// errdefer would belong here
} finally {
// defer would happen here
}The Zig one is so special and compiler-blessed that there is special syntax for defer blocks that only run when the function return is an error variant of that result ADT -- errdefer.
Generally, I find it isn’t necessary. I can usually figure out a way to make it work with standard flow control.
In my case, it’s the Swift language.
int x = 1;
defer x = 2;
return x; char *str = foo();
defer { free(str); }
return strlen(str);Right, there's a demonstration of GP's question (or a variation) on page 10 of the draft technical specification.