[HN Gopher] The Defer Technical Specification: It Is Time
___________________________________________________________________
The Defer Technical Specification: It Is Time
Author : mattjhall
Score : 116 points
Date : 2025-03-16 14:20 UTC (4 days ago)
(HTM) web link (thephd.dev)
(TXT) w3m dump (thephd.dev)
| Mond_ wrote:
| Seems like a perfect fit for C, and glad to see we're trying to
| avoid stepping into that funny pitfall Go has with its function-
| scoped defer keyword.
|
| Glad to see C is evolving and standardizing.
| codr7 wrote:
| I've been doing properly scoped defers in C since forever, as
| long as you have access to cleanup attributes and nested
| functions it's no big deal.
|
| https://github.com/codr7/hacktical-c/tree/main/macro
| wahern wrote:
| Yes, the proposal is tailored so that other than simple syntax
| support no new semantics need to be implemented within GCC to
| support defer, though clang will need to finally add support
| for nested functions--in spirit if not the literal GCC
| extension.[1] The proposal also gives consideration to MSVC's
| try/finally to minimize the amount of effort required there to
| support defer.
|
| [1] Because defer takes a _block_ , not a simple statement. And
| deferred blocks can be defined recursively--i.e. defer within a
| defer block.
| fuhsnn wrote:
| >the proposal is tailored so that other than simple syntax
| support no new semantics need to be implemented within GCC
|
| 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.
| Animats wrote:
| Ugh.
|
| 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.
| codr7 wrote:
| The point of defer is to put the cleanup logic in one place for
| local variables though, so the risk of someone else deleting it
| isn't a thing.
| jayd16 wrote:
| > It's "finally", in the try/finally sense of C++
|
| What sense is that? C++ doesn't have finally and the article
| explicitly calls out how its not like destructors.
| Animats wrote:
| Right, C++ doesn't have "finally". But "defer" defers to when
| a "finally" section would run in a language that has it. I
| think.
| IshKebab wrote:
| That's what it does in Go too. This is totally defer in the
| Go sense (except they fixed the scoping issue).
| Animats wrote:
| Not quite. _" The central idea behind defer is that,
| unlike its Go counterpart, defer in C is lexically bound,
| or "translation-time" only, or "statically scoped""_
|
| Defer in Go puts the deferred action on a run time to-do
| list that's processed at function exit. You can queue up
| deferred actions from a loop in Go. Not in this proposal
| for C.
|
| What happens in this C proposal if you put a defer
| request inside a loop? Is it a compile time error, do
| they somehow to try to give it meaningful semantics, or
| is it undefined behavior?
| gizmo686 wrote:
| Loops in C introduce a new lexical scope, so the defer
| runs once for every loop iteration.
|
| Assuming you are using defer for destructor purposes, and
| use it where you declare your variable, this would
| generally be what you want, as it frees the memory at the
| same time it goes out of scope.
|
| The flip side of this is that code like the following
| would be broken: char *foo = NULL;
| if (true) { foo = calloc(1,1);
| defer { free(foo); } } char c = *foo;
|
| As foo gets freed at the end of the conditional, which is
| prior to the dereference.
| IshKebab wrote:
| That particular example is easy - just put the defer
| before the if.
| sethaurus wrote:
| > What happens in this C proposal if you put a defer
| request inside a loop? Is it a compile time error, do
| they somehow to try to give it meaningful semantics, or
| is it undefined behavior?
|
| The action runs at the end of the loop-body, before the
| next iteration. It does this because the loop-body is the
| enclosing block, and a defer will always run when its
| enclosing block ends. As described in the article, this
| is intentionally-so and makes it possible to acquire a
| mutex inside a loop while automatically releasing it
| before the next iteration, something which is easy to get
| wrong in Go.
| IshKebab wrote:
| Yes maybe you didn't get to the end of my one-sentence
| comment but I did say
|
| > except they fixed the scoping issue
|
| > What happens in this C proposal if you put a defer
| request inside a loop?
|
| It executes at the end of the loop body.
| Jtsummers wrote:
| > The central idea behind defer is that, unlike its Go
| counterpart, defer in C is lexically bound, or "translation-time"
| only, or "statically scoped". What that means is that defer runs
| unconditionally at the end of the block or the scope it is bound
| to based on its lexical position in the order of the program.
|
| 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.
| chrsig wrote:
| yeah, I've always just extracted the loop body into a new
| function as a result
| infogulch wrote:
| I've created a lambda and called it inline to force lexically
| scoped defer semantics. Works fine and reads fine imo.
| chrsig wrote:
| I've definitely done that as well. I generally don't have a
| problem with it, but it makes for an extra conversation the
| first time someone sees the pattern.
|
| Given how some of the other ergonomic changes in go have
| gone (closures capturing loop variables, for instance), I'd
| support a change to lexical scoped defers if it were on the
| table.
| gblargg wrote:
| Block-based defer is also important when using macros that
| inserts blocks. They can use defer without care for how nested
| they are invoked.
| topspin wrote:
| Regarding the statements on golang's defer: "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.
| dgunay wrote:
| It's incredibly ugly but you could sort of hack in a smaller-
| scoped defer using anonymous functions:
| https://go.dev/play/p/VgnprcObPHz
| topspin wrote:
| Yeah, I get it. There's an idiom. Still, that glitch is
| guaranteed to catch everyone off guard, experienced or
| otherwise, when taking up golang. As I said, it's an entry on
| the list, and such a list exists for most (all?) mainstream
| languages. At least it's minor compared to nil, a flaw
| somehow promulgated in a brand new language many years after
| anyone purporting to be a language designer would or should
| have known to avoid. That's a mystery for the ages right
| there.
| hinkley wrote:
| I love the Principle of Least Astonishment, but I first
| encountered it in the Ruby book and I gave up reading it
| halfway through because I kept thinking, "He and I have _very_
| different definitions of astonishing... "
| Etheryte wrote:
| In a way that makes sense though, once you're at a level
| where you can not only write Ruby, but write books about
| Ruby, surely very few things would astonish you.
| hinkley wrote:
| That's why I always watch the newbies squirm trying to read
| my/our documentation.
|
| No feedback is ever as honest as unvarnished confusion.
| majormajor wrote:
| It's definitely a footgun but I think it's also pretty clear in
| go docs that defer is a function-return-time thing, versus a
| loop iteration thing. "A defer statement defers the execution
| of a function until the surrounding function returns." from
| https://go.dev/tour/flowcontrol/12
|
| 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.
| wruza wrote:
| My biggest astonishment is how people continue to shoot
| themselves in the foot by not making scope vs function
| declarations explicit. For the reasons that "someone will
| misunderstand complicated ideas, so let's make it implicit" or
| something. While there could be just: 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!
| zombot wrote:
| I wouldn't want my life turned into an embedded block,
| whatever that is.
| rollulus wrote:
| Say that the defer would execute inside for loops, what would
| make you more astonished: loops and functions are the
| exceptions, or defers execute at the end of _any_ block? I
| would prefer the latter of these two. But then the consequence
| is that a defer in an if-block executes instantly, so you
| cannot conditionally defer anymore. So it seems that the rules
| for when deferees execute need to be arbitrary, and "only
| functions" seems fewer exceptions than "only functions and
| loops", isn't it? And what about loops implemented through
| gotos? Oh boy.
| lmm wrote:
| > a defer in an if-block executes instantly, so you cannot
| conditionally defer anymore.
|
| 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).
| rollulus wrote:
| The parent I was replying to was talking about Go. There is
| no ternary operator in Go. There are no non-block forms of
| if in Go. I'm not sure what your "of course" is referring
| to, but I guess it is unrelated.
| lmm wrote:
| I figured we were talking about C since that's the main
| topic. In any case short-circuit && and || still work in
| Go AIUI, so my point stands.
| Someone wrote:
| > so you cannot conditionally defer anymore.
|
| 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?
| throw-qqqqq wrote:
| Another cool difference between this and Go's 'defer', is that it
| doesn't allocate memory on the heap. Go's 'defer' does and it has
| a small performance cost compared to just calling the .release()
| or whatever yourself... _shrugs_
|
| At least this was the case last I did benchmarks of my Go code.
| Dno if they changed that.
| klodolph wrote:
| Does go's defer allocate on the heap? I thought it would only
| do that if necessary.
| pkaye wrote:
| I know they implemented an optimization back in go 1.13. Not
| sure if that will help.
|
| https://github.com/golang/proposal/blob/master/design/34481-.
| ..
| zyedidia wrote:
| What is the recommended way to use defer to free values only on
| an error path (rather than all paths)? Currently I use goto for
| this: 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.
| lelanthran wrote:
| I don't even bother with `error1`, `error2`, ... `errorN`.
|
| 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.
| ayende wrote:
| That is a well structure system, yes Both cleanup for error and
| allocation happens in the same place
|
| That means you won't forget to call it, and the success flag is
| an obvious way to ha dle it
| loeg wrote:
| Can also use a different variable name for the success case and
| null out any successfully consumed temporaries.
| void* p1 = malloc(); if (!p1) return failure;
| defer { free(p1); } ... someOther->pointer =
| p1; p1 = NULL; return success;
| bobmcnamara wrote:
| I'm not sure I'd do either for this trivial case, but it might
| make sense where the cleanup logic is more complex?
| 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;
| lukaslalinsky wrote:
| Ever since I started working with Zig, I came to realization that
| its errdefer is even more useful than defer itself. But you can't
| implement errdefer in C, since there is no standard/disambiguous
| way of returning errors.
| infogulch wrote:
| Can you expand on how errdefer works in zig? I'm not familiar.
| Jtsummers wrote:
| https://ziglang.org/documentation/master/#errdefer
|
| 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 }
| loeg wrote:
| Zig has a special / compiler-known ADT for "value OR error".
| This is similar to Result<T,E> in Rust. Or in C++, e.g.,
| folly::Expected<T,E>.
|
| 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.
| fwlr wrote:
| The author takes great care to rebut a common theme among
| objections to the proposal - "this isn't necessary if you just
| write code better". I am reminded of this fantastic essay:
|
| > 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.
|
| (https://borretti.me/article/introducing-austral#goals)
| smadge wrote:
| I agree with that, but:
|
| - 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.
| globnomulous wrote:
| > is completely unreadable for what it does
|
| 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.
| majormajor wrote:
| Defer is often quite nice when you have many return paths but
| it also seems fairly limited if our goal is to assume human
| error is unavoidable. Ruby block-based stuff or "with" in
| Python seem like the clear winner there.
| zombot wrote:
| > they are traumatized by writing executable YAML
|
| It gives me solace to know that I am not alone.
| ChrisMarshallNY wrote:
| Pretty much the only time I use it, is if the act of doing some
| cleanup might cause a change (like a mutable function in a
| communication API, or letting go of a reference may interfere
| with a last operation).
|
| 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.
| Wumpnot wrote:
| Can't really blame MS for saying ..just use C++.. they aren't
| exactly wrong.
| neilv wrote:
| > _Here's a basic example showing off some of its core
| properties_
|
| 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.
| hyperhello wrote:
| I was thinking it might be clearer with defer printf("2");
| printf("1"); for example.
| gblargg wrote:
| Agreed, the example immediately made me see it as an example
| for the Obfuscated C contest.
| fuhsnn wrote:
| You can play with defer in Linux/VM with slimcc[1] today! It only
| diverges from the TS in keyword being _Defer, as well as several
| goto constraint violations not detected, bright side is you can
| witness why they are constraint violations...
|
| [1] https://github.com/fuhsnn/slimcc
| hyperhello wrote:
| What does this return? int x = 1; defer x =
| 2; return x;
| gizmo686 wrote:
| That will return 1. The defered code is executed after the
| return value is computed. This lets you do things like:
| char *str = foo(); defer { free(str); } return
| strlen(str);
| Jtsummers wrote:
| https://thephd.dev/_vendor/future_cxx/technical%20specificat.
| ..
|
| Right, there's a demonstration of GP's question (or a
| variation) on page 10 of the draft technical specification.
| sbrudenell wrote:
| I always thought golang's defer was a readability nightmare
| because it obfuscates execution order. OP's "basic example" is
| ... a great example of obfuscation. try/finally doesn't have this
| problem. It can add indents, but I'd so much rather read a
| function with 4 indents than 4 defers
| aeijdenberg wrote:
| The TS doesn't seem to provide for a way to modify return values
| for the function. For example the following is a common pattern
| in Go using defer to ensure that errors closing a writeable file
| are returned: 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 }
___________________________________________________________________
(page generated 2025-03-20 23:02 UTC)