Implementing "defer"(c3.handmade.network)
c3.handmade.network
Implementing "defer"
https://c3.handmade.network/blogs/p/7641-implementing_defer
49 comments
The D Language has scope(exit), instead of defer. Example:
scope(exit) writeln("3");
writeln("1");
writeln("2");
Output: 1
2
3The article references 'defer' being in several newer languages, but seems to have missed the 'scope' implementation in the D language which I think was implemented long before the other languages got their 'defer' feature.
No, I'm aware of it. GCC also has a similar feature as a C extension. I picked those languages because they very clearly use the same keyword.
I'd like "defer_func" to be an option; I do have some code that seriously uses it that way. But all in all defer defaulting to scope would be a lot more useful.
I've written some functions-in-functions solely to get "scope-like" deferral. Now, so far, none of them have ever survived refactoring, in that they've always been lifted up to become their own top-level functions where defer resumes working like I wanted anyhow. But I've also got a ton of code where the difference doesn't much matter (a function with only one scope, or where defer is only on the outer-most scope anyhow), and it still seems like scope would be a better default.
I've written some functions-in-functions solely to get "scope-like" deferral. Now, so far, none of them have ever survived refactoring, in that they've always been lifted up to become their own top-level functions where defer resumes working like I wanted anyhow. But I've also got a ton of code where the difference doesn't much matter (a function with only one scope, or where defer is only on the outer-most scope anyhow), and it still seems like scope would be a better default.
I've written this a few times:
And this is also what Go missed. I'm a big fan of RAII.
class defer{public:defer(std::function<()> f):f_(std::move(f)){} ~defer(){f_();}private:std::function f_;};
Then fd = socket(…);
defer _([]{close(fd);});
But yeah, most of the time a refactor will instead create a proper wrapper class. It's rare that I want to RAII something, but only in some cases. E.g. I probably want RAII on FDs always, with proper ownership, not merely in some cases.And this is also what Go missed. I'm a big fan of RAII.
No need to box with std::function. Since C++17 this is enough:
edit: a more complete implementation: https://godbolt.org/z/6675K1 . I like to name my guards. Making them anonymous is left as an exercise for the reader.
template<class Fn>
struct defer {
Fn fn;
~defer() { fn(); }
};
template<class Fn>
defer(Fn) -> defer<Fn>;
int main() {
defer _ { [] { std::cout << "exit"; }};
}
Although you might want to add the ability to dismiss() the guard.edit: a more complete implementation: https://godbolt.org/z/6675K1 . I like to name my guards. Making them anonymous is left as an exercise for the reader.
Yeah, I chose not to templatize and add dismiss() mostly to make it fit better in a comment.
But like I said at some point, if it gets complicated, then probably the value should be properly wrapped, with actual owner semantics rather than defers.
But like I said at some point, if it gets complicated, then probably the value should be properly wrapped, with actual owner semantics rather than defers.
You are right in most cases. I find it useful for transactional code though, where I might be modifying a bunch of data structures and I want to undo everything on error. Doesn't really make much sense to write classes in this case if the code is really function specific.
Slightly OT: I don't like defer much, it's just another way to do automated RAII style cleanups. It has slightly different tradeoffs than RAII (it's more of an "in-line" way of coding things) but in the end it's another high-level feature that obfuscates the actual control flow.
To be clear, I don't think that chains of cleanup-labels with corresponding gotos are better. As a long-time OOP basher, I find myself using more and more explicit state structures with "object" semantics: There's a destructor that cleans the state up from every possible configuration except "uninitialized". As far as possible, objects should already be considered initialized when cleared to 0. (This idiom is called ZII, or zero-is-initialization).
A problem with defer is that if one has to split what the function does in multiple functions, defer breaks, and one needs a completely different solution (explicit state structure). Not to praise it too much but with RAII you don't pay that cost.
In other words, as so many language features, defer offers to save early typing work early, at the expense of later friction, or even of requiring a re-write, when the times comes to solidify the code structure.
Disclaimer: I've never actually typed the word defer in any programming language. :-)
To be clear, I don't think that chains of cleanup-labels with corresponding gotos are better. As a long-time OOP basher, I find myself using more and more explicit state structures with "object" semantics: There's a destructor that cleans the state up from every possible configuration except "uninitialized". As far as possible, objects should already be considered initialized when cleared to 0. (This idiom is called ZII, or zero-is-initialization).
A problem with defer is that if one has to split what the function does in multiple functions, defer breaks, and one needs a completely different solution (explicit state structure). Not to praise it too much but with RAII you don't pay that cost.
In other words, as so many language features, defer offers to save early typing work early, at the expense of later friction, or even of requiring a re-write, when the times comes to solidify the code structure.
Disclaimer: I've never actually typed the word defer in any programming language. :-)
> Go has it's own special defer which only fires on function end, otherwise defer has consistent "execute at scope end" semantics.
Does anyone know why Go’s `defer` semantics is so unintuitive? Why is `defer` function-based rather than block-based, in a language where variables are block-scoped?
Does anyone know why Go’s `defer` semantics is so unintuitive? Why is `defer` function-based rather than block-based, in a language where variables are block-scoped?
If I may venture a guess, it is because this way there is no limit in regard to jumps within or out of a function. Since Go retains `goto`, function based is easier to implement. In addition, non-local jumps, such ones constructed by setjmp, are fairly easily easy to resolve (just say that the jump will release all queued defers before the jump).
But I've also heard the opinion that some find the function based `defer` easier to reason about, so maybe that weighs in as well.
But I've also heard the opinion that some find the function based `defer` easier to reason about, so maybe that weighs in as well.
I suspect that it’s a side-effect of Go’s origin in the Plan 9 compiler chain. This is really just speculation, but I’ll try to justify it.
First, early versions of Go strictly required a “return” as the final statement in a function, rather than just checking that each branch terminated successfully (i.e. you couldn’t return from both branches of an if/else, you also needed a redundant return statement afterwards). So I think initially the compiler didn’t perform the kind of escape analysis that would make block-scoped “defer” easy to implement.
Second, block-scoped defer is really equivalent to RAII via constructors and destructors in C++. Go was specifically designed as an anti-C++, an attempt to return to the roots of C and improve on it, so the designers would have been very wary of copying C++ features like RAII.
I think it was a mis-step, and they should have gone with block-scoped. Other languages have copied Go’s syntax, but they’ve uniformly opted for C++’s semantics, which in this instance are much easier to reason about (definitely not always the case with C++!)
First, early versions of Go strictly required a “return” as the final statement in a function, rather than just checking that each branch terminated successfully (i.e. you couldn’t return from both branches of an if/else, you also needed a redundant return statement afterwards). So I think initially the compiler didn’t perform the kind of escape analysis that would make block-scoped “defer” easy to implement.
Second, block-scoped defer is really equivalent to RAII via constructors and destructors in C++. Go was specifically designed as an anti-C++, an attempt to return to the roots of C and improve on it, so the designers would have been very wary of copying C++ features like RAII.
I think it was a mis-step, and they should have gone with block-scoped. Other languages have copied Go’s syntax, but they’ve uniformly opted for C++’s semantics, which in this instance are much easier to reason about (definitely not always the case with C++!)
> Go was specifically designed as an anti-C++
Ha! And still they managed to reimplement RAII and exceptions, only very very poorly.
Ha! And still they managed to reimplement RAII and exceptions, only very very poorly.
defer also is used to recover from panics, which makes much more sense in a function end context.
For example, when writing code that performs an operation with the potential to panic, but that you want to convert to an error, something you can do is name the error return value and then modify it in the defer, after recovering from a panic, according to the cause of the panic. It's not obvious that that pattern would work well if defer could also execute on scope end - what does recover do then? Will it affect the following recovers? Also, if the function continues executing but you modified some variables, it makes it complicated to reason about what could be in those variables.
Defer running at function end is much more easier to reason about: functions execute their standard control flow until a return or panic is hit, and then defers are executed, without returning to standard control flow.
For example, when writing code that performs an operation with the potential to panic, but that you want to convert to an error, something you can do is name the error return value and then modify it in the defer, after recovering from a panic, according to the cause of the panic. It's not obvious that that pattern would work well if defer could also execute on scope end - what does recover do then? Will it affect the following recovers? Also, if the function continues executing but you modified some variables, it makes it complicated to reason about what could be in those variables.
Defer running at function end is much more easier to reason about: functions execute their standard control flow until a return or panic is hit, and then defers are executed, without returning to standard control flow.
One benefit of function based defer is that you can use anonymous functions to create hacky block-based defers.
If the language only has block based defers then that's it. You have that and only that.
What if I create a resource in an if block and want to defer closing it when the function ends?
If the language only has block based defers then that's it. You have that and only that.
What if I create a resource in an if block and want to defer closing it when the function ends?
Depending on how the defer implementation looks like the same could be achieved with block-level defer by having the defer's "owner" (e.g. some object with RAII semantics) in the outer scope.
You can get function based defers pretty easily with block ones too
func Deferred() {
defers := []func(){}
defer func() {
for _, def := range defers {
def()
}
}
for {
cleanup, done := work()
defers = append(defers, cleanup)
if done {
break
}
}
}
Edit: this is how defers were implemented in the compiler until a few versions ago and will fall back to this if you defer in a loopI assumed it was performance, my understanding is it keeps a stack of statements to execute a function end. Perhaps the cost was too high to do that at the end of each block.
This is not about (runtime) performance. Like the the article mentions, the block-end version lets you implement everything without dynamic allocations and (I guess) pointer de-reference. The C3 implementation of block-end defer described in the article is zero-cost, as far as I can see.
I suspect the real reason is the driving force behind a great deal of the early Go 1.x design choices: making the compiler simpler. Or, if you wish: _compiler_ performance.
Go 2.x is shaping up to be a different beast, but Go 1.x had an extremely strong design philosophy favoring compiler simplicity (which often equates to compilation speed) over nearly every other concern, including runtime performance. That's exactly one of the main argument that was used against having generics: (non-erased) generics significantly improve performance, but slow down the compiler.
I'm personally very much in disagreement with this philosophy, but I don't think all these design choices are baffling or even misguided - they clearly achieve what they were set to do.
I suspect the real reason is the driving force behind a great deal of the early Go 1.x design choices: making the compiler simpler. Or, if you wish: _compiler_ performance.
Go 2.x is shaping up to be a different beast, but Go 1.x had an extremely strong design philosophy favoring compiler simplicity (which often equates to compilation speed) over nearly every other concern, including runtime performance. That's exactly one of the main argument that was used against having generics: (non-erased) generics significantly improve performance, but slow down the compiler.
I'm personally very much in disagreement with this philosophy, but I don't think all these design choices are baffling or even misguided - they clearly achieve what they were set to do.
I think the reason is much simpler: block-scoped defer isn't useful if you statements appear in a nested construct. Example:
fd := OpenSocket(...);
if fd != 0 {
defer CloseSocket(fd);
}
// do stuff
For the same reason, Go's defer cannot be implemented without dynamic allocations. For example, how would you implement: func foo(int n) void {
for i := 0; i < n; i++ {
var res = bar(n);
defer fmt.Println(res);
}
}
This calls bar(0), bar(1), etc. then prints the results of bar(n - 1), bar(n - 2), etc. There is no way to implement this without some way to store the intermediate values.Compiler should be able to figure out at compile time whether the dynamic allocation is needed. But maybe the Go compiler already does this, I don't know.
True, I was talking about the general case.
This is a bit similar to local variables: they have to be allocated on the heap in the general case, but Go uses escape analysis to determine which local variables can safely be allocated on the stack instead, which is much cheaper.
This is a bit similar to local variables: they have to be allocated on the heap in the general case, but Go uses escape analysis to determine which local variables can safely be allocated on the stack instead, which is much cheaper.
"my understanding is it keeps a stack of statements to execute a function end."
The official Go compiler a couple of versions back optimized this away to the obvious (if you know about compilers) mapping of region to what defers need to be run on exit and can compile them in now. If your function uses it like a stack, it'll use a stack like it used to, but it optimizes the common case of a handful of defers in the main body now.
The official Go compiler a couple of versions back optimized this away to the obvious (if you know about compilers) mapping of region to what defers need to be run on exit and can compile them in now. If your function uses it like a stack, it'll use a stack like it used to, but it optimizes the common case of a handful of defers in the main body now.
Nim's defer just wraps the current scope in a try/finally, with the deferred code running in the finally. It is probably better just to use try/finally directly because it's more explicit about what is in the try block. It's not worth it to obscure that just to avoid a new level of indentation...
To me, the benefit of defer over try/finally is code locality: You can easily see whether some action has an associated cleanup, because the 'defer' statement is just before/after the action. With try/finally, the cleanup (which has to be in sync with the action) might be outside your editor window.
Came here to note this exact difference with respect to finally
Consider this code
Consider this code
fp = os.open(x) // imagine file open
defer fp.close()
fp.read()
With defer, one would think I can simply wrap this in a for-loop if I want to open and read a bunch of files. Go doesn’t promise this, but not clear until linter complains. In languages were it “ends at scope”, this is still wrong. If we wrote it as finally, dev would know finally is outside the loop or they need to wrap in another sub-scope inside the loop {}In languages were it “ends at scope”, this is still wrong.
I don’t quite follow -- what would go wrong? If you put that in a loop body, won’t you get one open() and one close() per iteration, as intended?
I don’t quite follow -- what would go wrong? If you put that in a loop body, won’t you get one open() and one close() per iteration, as intended?
Not the commenter but which one of these is the defer generating?
// option 1
try {
fp = os.open(x)
fp.read()
}
finally {
fp.close()
}
// option 2
fp = os.open(x)
try {
fp.read()
}
finally {
fp.close()
}
Should we close if open fails? Maybe, maybe not, but with try/finally it is obvious which one it is doing.The first one doesn't compile in any language that requires variables to be initialized before use.
In nim, it generates code analogous to option 2.
You sure? https://forum.nim-lang.org/t/4022#25046 edit: the docs seem to say you're right, maybe that forum post is outdated but it's from nim's author.
The first one is obviously wrong, which I mean in the sense that anyone who knows what defer is will know that, not in the sense that you've posted an obviously bad post (it is a fair question!). defer is a statement, not a declaration, and does not take effect until it is executed like any other statement. It follows standard structured programming rules; line 2 does not execute until line 1 is done. (A rule so simple and obvious we often don't think about, especially since structured programming has basically won and everything we use nowadays is structured, but it is still a rule that we use in programming.)
What I meant was if I am Go developer and I write
With finally, things are clear, I think.
for /* whatever */ {
fp = os.open(x) // imagine file open
defer fp.close()
fp.read()
}
This is wrong. Go linter might tell you it is wrong, but
a) the defers are pilling up and you might run into too-many-files open
b) I can never remember if `fp` is saved in defer closure by reference or value. That is, at the end, even if inefficient, are all pending defers closing the same pointer?
Now in a language where "it ends in scope", the scope hasn't ended until the loop exists. Now in the RAII world, the `fp` being overwritten would have saved the day by automatically closing it, but we are not talking about RAII world.With finally, things are clear, I think.
Now in a language where "it ends in scope", the scope hasn't ended until the loop exists. (I assume you mean exits there?)
No, I don’t think that’s correct, in any mainstream language. Each iteration of the loop body is a separate scope. If you declare a new variable inside the loop body, it lives until the end of that iteration then falls out of scope. Next time around the loop, you declare a new, separate variable.
There is a slight grey area around the loop header -- exactly when does the scope start and end? Older C compilers used to disagree about this, but the rules were firmed up in C++ (and I assume in recent versions of C too) and now loop headers use the tightest scope they can.
So I would expect “defer” to run at the end of each iteration, exactly the same as the C++-style RAII case, and that is in fact how it works in every modern language except Go.
Another way to think about it that might be helpful: most languages try to implement defer in a completely static way, where just looking at the syntax, you can figure out exactly where and when defer handlers are going to run. You can allocate all the storage you need at the start of the function, and nothing tricky is required at runtime. If defer handlers are queued up and run as a batch later on, that’s dynamic behavior that needs some extra runtime support, and that’s why most languages don’t do it.
No, I don’t think that’s correct, in any mainstream language. Each iteration of the loop body is a separate scope. If you declare a new variable inside the loop body, it lives until the end of that iteration then falls out of scope. Next time around the loop, you declare a new, separate variable.
There is a slight grey area around the loop header -- exactly when does the scope start and end? Older C compilers used to disagree about this, but the rules were firmed up in C++ (and I assume in recent versions of C too) and now loop headers use the tightest scope they can.
So I would expect “defer” to run at the end of each iteration, exactly the same as the C++-style RAII case, and that is in fact how it works in every modern language except Go.
Another way to think about it that might be helpful: most languages try to implement defer in a completely static way, where just looking at the syntax, you can figure out exactly where and when defer handlers are going to run. You can allocate all the storage you need at the start of the function, and nothing tricky is required at runtime. If defer handlers are queued up and run as a batch later on, that’s dynamic behavior that needs some extra runtime support, and that’s why most languages don’t do it.
> Each iteration of the loop body is a separate scope.
Thanks for the correction. You are right. I wasn’t thinking straight.
Thanks for the correction. You are right. I wasn’t thinking straight.
[deleted]
Here my C++ defer implementation https://twitter.com/stevemk14ebr/status/1347408381301190658?...
The problem isn't that C++ can't do "defer"... the problem is that C++ can do everything, and all those "everythings" are constantly interacting because code bases will use all that "everything" fairly routinely. Just glancing at a particular defer implementation won't tell you how it interacts with everything else, let alone how it will interact with whatever strange things your own codebase has.
Go's defer function is very predictable, not so much because Go's defer is magic but precisely because it isn't. There aren't a ton of features for it to interact with.
This also isn't a special virtue of Go; IMHO it's a special (not unique, but unusual) failing of C++. It was insanely complicated already in the 90s and every standard since then has only made it worse.
Go's defer function is very predictable, not so much because Go's defer is magic but precisely because it isn't. There aren't a ton of features for it to interact with.
This also isn't a special virtue of Go; IMHO it's a special (not unique, but unusual) failing of C++. It was insanely complicated already in the 90s and every standard since then has only made it worse.
That one executes deferred statements at end of scope, instead of on function exit. That doesn't work well if your defer-statement occurs in an if-body, for example.
I don't think it's super valuable to mimic the Go syntax anyway. In C++ you can fairly easily implement a class that works like this, which is fairly clean and intuitive:
I don't think it's super valuable to mimic the Go syntax anyway. In C++ you can fairly easily implement a class that works like this, which is fairly clean and intuitive:
void func() {
Deferred deferred;
for (int i = 0; i < 10; ++i) {
deferred.push([] { some callback });
}
// when Deferred is destroyed, it executes callbacks.
}
This is somewhat more powerful than Go's defer in that it gives the user explicit control over the scope. I think in Go you'd have to create a closure and call it immediately to create a nested scope for the purposes of executing deferred statements early.Boost has had the BOOST_SCOPE_EXIT macro at least since Boost 1.38 (February 2009), a few months before the first public announcement of Go. Judging by the copyright, the earliest versions of this library (before acceptance into Boost) are from 2006, before Pike et. al. even started working on Go internally in Google. BOOST_SCOPE_EXIT in turn is based on Andrei Alexandrescu's pre-closure ScopeGuard implementation from 2000.
Granted, both BOOST_SCOPE_EXIT and your defer macro work at block end, not function end, but I agree with OP that this is practically always the better approach, so I don't think we should look at this thing as C++ trying to copy go features with lousy hacks. It's probably the other way around, if anything. And modern implentations in C++ (like yours) are extremely straightforward.
Granted, both BOOST_SCOPE_EXIT and your defer macro work at block end, not function end, but I agree with OP that this is practically always the better approach, so I don't think we should look at this thing as C++ trying to copy go features with lousy hacks. It's probably the other way around, if anything. And modern implentations in C++ (like yours) are extremely straightforward.
std::shared_ptr<void> foo(nullptr, [&bar] (void*) { printf(":) %s\n", bar); });
the downside is having to write nullptr instead of just the closure. Something like this really should be part of the standard.
the downside is having to write nullptr instead of just the closure. Something like this really should be part of the standard.
Achieve maximum overkill with the generalisation of this concept in Raku. "defer" looks like a child's toy in comparison.
https://docs.raku.org/language/phasers
https://docs.raku.org/language/phasers
Indeed! :-) FWIW, the "defer" functionality is implemented as the LEAVE phaser in Raku: https://docs.raku.org/language/phasers#index-entry-Phasers__...
Might be useful to mention '.. in Go' in the title.
I clicked on it with the assumption it was about loading resources in `script` tags with the defer or async attributes.
I clicked on it with the assumption it was about loading resources in `script` tags with the defer or async attributes.
It's almost never what you want, and it requires terrible gymnastics whenever the difference matters.
So in C++ implementing defer the right way is to just register a std::function that gets run from the class destructor. I've used it as an alternative to wrapping a resource, or timing a function call.
Only gotcha is to be careful about throwing from within the destructor. Or to put another way: don't.