23 ms·
A Defer Mechanism for C
- greatgib 6y agoDespite being convenient,I have the feeling that it might be a bad idea. One of the key interest of C compared to more recent languages is that everything is explicit. With a finger you can follow the code as it runs and know exactly what is going on and when exactly. At the opposite, there is c++ that does a lot of things automagically. And it is often hard to understand why you suddenly get a segfault out of blue, just because a destructor was randomly called at the wrong time.
- coldtea 6y ago>One of the key interest of C compared to more recent languages is that everything is explicit. There's nothing implicit about defer.
- tangus 6y agoDeferred actions are not explicit at the exit points. That's quite implicit. You can no longer reason about a local piece of code; you now have to know its lexical nesting up to top level to see if it's inside a guard block that might trigger hidden behavior.
- coldtea 6y agoThat's still local at the scope level though, which is quite acceptable. Plus, to handle the free or the leak if you had forgotten to free a resource at the exit point would also require to "know its lexical nesting up to top level". Between goto and longjump and co, C has much worse non-local behavior than defer.
- josephcsible 6y agoGoto isn't nonlocal. You know you'll only every jump from them, and that you'll only ever jump to the specified label.
- coldtea 6y agoThe specified label is what makes goto no-local. You need to check the whole codebase to find out where you'll land. By your definition only [1] "come from" would be non-local. [1] https://en.wikipedia.org/wiki/COMEFROM https://en.wikipedia.org/wiki/COMEFROM
- josephcsible 6y ago> the whole codebase You can't goto out of a function, and you know there's exactly one such label inside it. If goto isn't local, then neither are function calls, since the function could be defined anywhere in the codebase.
- coldtea 6y ago>You can't goto out of a function You can with a goto expression and a label address available - though the behavior is undefined in C, so bets are off. And you can with longjump/setjump more explicitly.
- josephcsible 6y agoAre you talking about this? https://gcc.gnu.org/onlinedocs/gcc/Labels-as-Values.html https://gcc.gnu.org/onlinedocs/gcc/Labels-as-Values.html It's a GNU extension, not part of standard C at all.
- gpderetta 6y agoFWIW, Normally the term "non local goto" is reserved for control transfer beyond the current activation frame. So C goto is strictly local, bit longjmp would be non-local.
- sramsay 6y agoI wondered about this. Say I have a block, at the end of which I free a bunch of memory. I also have a bunch of other exit points within the block (mostly for catastrophic errors, say). Would a deferred free only apply to the outer scope? Because if so, I really don't see the point at all.
- dnautics 6y agoyes, you always run defer. I don't see it in this proposal, but Most languages/frameworks that implement a defer also implement a "error defer" which only gets triggered on some sort of labeled early exit scenario, and some implement "success defer": (i believe this is scopeguard: https://www.youtube.com/watch?v=WjTrfoiB0MQ https://www.youtube.com/watch?v=WjTrfoiB0MQ)
- dnautics 6y agoit's control flow. What you are saying is like saying "while loops are implicit: you can no longer reason about code; you now must think about the state of the check boolean to see if it will trigger the hidden behaviour of going back to the top of the loop because there's no explicit "go back to the top of the block" statement.
- josephcsible 6y agoWith a while loop, to know what happens at the bottom of the block, you only need to check the top of the block. With defer, to know what happens at the bottom of the block, you need to check the entire contents of the block.
- IAmADHDToo 6y ago> With a while loop, to know what happens at the bottom of the block, you only need to check the top of the block. Anyway, what could possibly happen at the end of a while loop, besides going back to the begining for the test? Besides, if you have a bug in your code, you will have to look at the whole block anyway.
- chongli 6y agoWhat? Any statement inside a while loop could mutate the loop variable and then all bets are off. You have to check every line of a while loop to know what’s going on. Heck, another thread could hold a pointer aliasing the loop variable and then mutate it, causing the loop to terminate for no obvious reason.
- josephcsible 6y agoMy point is that you know at the end of the while loop, it's going to test the loop variable you wrote at the beginning, no matter what's in the loop body. The fact that other things could change the variable isn't relevant. With defer, you don't know what will happen at the end of a block without looking through the whole thing.
- SPBS 6y agoIt's as much hidden behaviour as destructors being called magically on an object that goes out of scope. In practice it doesn't hinder understandability as much as you think.
- josephcsible 6y ago> destructors being called magically on an object that goes out of scope Which C doesn't have.
- dragonwriter 6y ago> Deferred actions are not explicit at the exit points. They are no less explicit than the actions performed at the end of iterations of for or while loops. In general, for C, understanding the behavior of code requires understanding “is it in a block, and if so what kind of block”; guard blocks would be not generally different.
- apankrat 6y agoEvery defer statement is ultimately pushing a lambda into some stack that is executed when the defer block is exited. With an exception of setjmp/longjmp, I can't think of an existing C construct with similar run-time complexity.
- IAmADHDToo 6y agoThat's indeed tricky: if there's a for loop inside the guarded block which generates many defers, a stack would have to be created to handle them somehow.
- saagarjha 6y agoThe defer would just run at the end of each iteration.
- deleted 6y ago[deleted]
- skohan 6y agoIs it actually a lambda, or is it just essentially appending statements to the end of the scope?
- anfilt 6y agothat depends for instance you can safely call free on NULL so you could just append. However, for some things you can only free/return resources if you successfully created that resource. At which point you would need to use something like a stack.
- skohan 6y agoI think the broader and more accurate point is that defer adds cognitive load to reasoning about the order of execution. It's true that defer (at least everywhere I've seen it executed) is totally explicit and deterministic, but in the case of multiple defers in the same block it can take some thought to reason through exactly what happens when.
- zabzonk 6y agoDestructors are not called randomly.
- sillysaurusx 6y ago"Smart pointers go brr" It's not really random, but it's quite hard to follow.
- lultimouomo 6y agoThat's really an argument against smart (to be precise, shared) pointers, not against destructors.
- ben509 6y agoIf it's too cold for you, it's too cold for your smart pointers. Bring your smart pointers inside during the winter months.
- eropple 6y agoIt shouldn't be. std::unique_ptr is very clear. (std::shared_ptr should be used only when absolutely required and should be kept under close watch the whole time.)
- djmips 6y agosearches codebase I'm working on... finds thousands and thousands of std::shared_ptr (many involve a typedef, so there are even more...) eep! Should I be alarmed?
- hedora 6y agoYes. On reflection, or on ramping up new developers, I think you’ll find that it is unnecessarily difficult to reason about object lifetimes. There’s a good chance that most of the shared_ptr’s can be replaced by unique_ptr, which has less runtime overhead. More importantly, it documents the intent of the programmer regarding ownership semantics, and the timespan during which the object should be remain valid.
- yvdriess 6y agoguard/defer is semantically and syntactically as explicit (or implicit) as C's automatic storage class (https://en.cppreference.com/w/c/language/storage_duration https://en.cppreference.com/w/c/language/storage_duration), which is the default storage class.
- fanf2 6y agoOnly if you include dynamically sized arrays, which are deprecated because of their problematic allocation behaviour.
- shakna 6y agoI don't believe that VLAs are actually deprecated as such, but have been moved to an "optional" feature for implementation in C11. There's a feature macro to check, __STDC_NO_VLA__, but I don't think the feature is actually disappearing from the standard any time soon.
- cassepipe 6y agoI wonder what is a good use for VLAs btw. I used it for a string padding function to create the padding with a simple loop before a call to strjoin. Is that a bad idea or a legitimate use case?
- saagarjha 6y agoDon't use VLAs for anything that actually requires an allocation. However, VLAs are quite useful when you have a buffer and want to access it like an array.
- brudgers 6y agoDespite being convenient,I have the feeling that it might be a bad idea. What is "I am thinking of using C?"
- dkersten 6y agoDepending on your platform, it may be your only reasonable choice.
- brudgers 6y agoI agree completely. For the most part it seems if a person has to think long and hard about it, they probably should not use C and if it is the obvious choice that’s probably because C is ordinary in the context. These days there are probably fewer edge cases than in the past.
- whatshisface 6y ago>One of the key interest of C compared to more recent languages is that everything is explicit. With macros this is not exactly true. I gesture towards the GObject system for an extremely complex situation in big important production software.
- quelsolaar 6y agoI agree. I have tried to come up with some reasonable way to constrain macros, but its really hard!
- whatshisface 6y agoThe way to do it is, survey your macro use to identify common patterns, specify them formally, and then write a computer program that allows those patterns specifically but no others. In other words, design a higher-level language. ;)
- desiderantes 6y agoSomething like https://wiki.gnome.org/Projects/Vala https://wiki.gnome.org/Projects/Vala ?
- dnautics 6y ago> With a finger you can follow the code as it runs and know exactly what is going on and when exactly. Oh yeah? https://www.youtube.com/watch?v=Gv2I7qTux7g?t=29m21s https://www.youtube.com/watch?v=Gv2I7qTux7g?t=29m21s
- Someone 6y agoSo, what’s the alternative? Not freeing resources is a very common error; it would be nice if the compiler could help detecting it. A possible explicit solution I can think of is to introduce a new function attribute that gives the name of their ‘cleanup’ function (so that the compiler would know fopen needs a fclose, for example) and a compiler that uses these attributes to issue a warning if a function has a path that calls a function and doesn’t either call its cleanup function or returns its result. I don’t know whether that would cover all bases, though.
- Asooka 6y agoI view it as another element of structured programming. It can be expressed as a series of "goto" statements, same as for, while, do, switch. It's just one they didn't think of back when C was being designed. If it was in from the beginning, nobody would think it strange. That said, it's still debatable if it's useful, given that you can achieve the same thing with the struct some_resource resource; do { resource = allocate(...); if (!resource) break; } while(0); if (resource) dispose(resource); I can see it as a good thing because you have the dispose statement next to the allocate statement, which makes the logic easier to follow, but the implementation may have caveats which make it actually harder to reason about, e.g. see the other thread about capture value vs capture reference - C will most probably need to capture by reference, which means that modifying "resource" later on changes the meaning of the deferred statement.
- rseacord 6y agoI've heard this comment more than once. What we are trying to accomplish is to collocate the resource acquisition code with the acquisition release code. This does mean that the release code is removed when where the release occurs (for example, at every location a function returns. However, this is not without precedence in C. For example, just look at the for loop: for (clause1; expression2; expression3) statement expression3 is executed after statement.
- simias 6y agoI would argue that the C for loop is a rather awkward construct. It's found its way in many languages, so I think most people are used to its idiosyncrasy but it's not great if you try to take a fresh look at it. I think the best defense of this syntax is that it makes writing basic iteration a bit nicer without having to add boilerplate (the iconic `for (i = 0; i < n; i++)`) but then I would argue that the real problem is that C is severely lacking in the iteration department and this is a rather obvious hack (that languages like Javascript felt the need to copy wholesale, for some insane reason).
- scythe 6y agoThe nonlocality can be eliminated by replacing the `guard {}` block with a `resolve;` statement to be placed at the end of a block containing multiple `defer`s. This also reduces nesting and solves the question of where the `defer` is executed and makes it easy to grep to the location where the `defer` statement will be executed. Of course, it would be a syntax error to use `defer` without a following `resolve;`.
- pornel 6y ago"Explicit" in language design is catch-all term for several different things: https://boats.gitlab.io/blog/post/2017-12-27-things-explicit-is-not/ https://boats.gitlab.io/blog/post/2017-12-27-things-explicit... (I highly recommend this article.) Defer is still manual and local, just less noisy and burdensome. Just like every C feature: if you know how it's implemented (and optimized), you will know what is going to happen. All big C compilers already support stack frames and unwinding, so it's not even entirely novel functionality. While you may object it's not entirely obvious how unwinding is going to be implemented, OTOH in functions with complex control flow it can be easier to understand which `defer`s are going to be run, as opposed to following nested `else` statements, or reasoning about the program state from all `goto cleanup` locations.
- sudobash1 6y ago> One of the key interest of C compared to more recent languages is that everything is explicit. With a finger you can follow the code as it runs and know exactly what is going on and when exactly. I would suggest functions like atexit and pthread_cleanup_push as well established counterexamples. Granted this is not a perfect comparison because these are implementable without extending the c language; however, I think they have the same general idea of "defering" cleanup. I think the proposed defer functionality is actually more readable because the defer command will be written much closer to where it will be exited. Compare this to atexit() which may be put anywhere.
- btilly 6y agoOne of the key interest of C compared to more recent languages is that everything is explicit. With a finger you can follow the code as it runs and know exactly what is going on and when exactly. This has not been true in many years. C appears to be such a language, but optimizing compilers have learned how to find and optimize undefined behavior in code that most C programmers don't realize is unsafe. As a result there can be a considerable gap between the code as clearly intended, and the code that will be generated. See https://blog.regehr.org/archives/213 https://blog.regehr.org/archives/213 for more on this. Including real world examples of things like validation checks being elided by the compiler, leading to real-world vulnerabilities in programs which clearly have checks to avoid exactly those vulnerabilities.
- Animats 6y agoDespite being convenient, I have the feeling that it might be a bad idea. Probably. It's one of Go's lesser ideas. C already has a "defer" mechanism in "exit", to close out files and such. Of course, the final I/O status gets lost.
- Technically 6y ago> One of the key interest of C compared to more recent languages is that everything is explicit. With a finger you can follow the code as it runs and know exactly what is going on and when exactly. I don't agree! There's a lot of behavior that's implicit and you essentially need to internalize the C standard/compiler behavior to follow. Weak typing, for instance; defaults for memory access/fencing, handling faults, runtime semantics with respect to initialization of the process, cleanup via atexit, and signal handling. Memorable, sure, but hardly explicit.
- CyberRabbi 6y agoI’ve always considered “defer” an inelegant kludge in comparison to RAII for automatic cleanup of resources. It’s surprising that the C standards committee is considering adding that to the language. Then again, nearly none of the syntactic C features post-C89 have been very compelling or widely adopted.
- kangalioo 6y agoDefer is more explicit than RAII, so a better fit for the very explicit C languag
- kangalioo 6y agoDefer is more explicit than RAII, so a better fit for the very explicit C language
- CyberRabbi 6y agoIt’s even more explicit to just call your cleanup routines manually, so by that logic “defer” is inferior to existing methods of handling cleanup in C
- gpderetta 6y agosubroutine calls requires implicit allocation and deallocation of stack frames. Real Programmers™ use goto and manage the stack by hand.
- drsozesakamoto 6y ago> subroutine calls requires implicit allocation and deallocation of stack frames. Real Programmers™ use goto and manage the stack by hand. Could u point to an example of programmers doing this ?
- gpderetta 6y agosorry, I had forgotten the /sarcasm tag. Although I guess Cheney on the MTA counts.( https://dl.acm.org/doi/10.1145/214448.214454 https://dl.acm.org/doi/10.1145/214448.214454 )
- elitepleb 6y agoJumping back in C always means trouble.
- kzrdude 6y agoDefer is a jump forward (at error points, to scope end) but it's declared in-line.
- coldtea 6y agoIsn't this already available as a GCC/LLVM extension? I know several codebases that use it in that form. Good to see it standardized of course...
- delusional 6y agoI don't think defer is really "available", but it can be implemented as a macro and the use of some non-standard extensions.
- coldtea 6y agoYes, as a macro and extensions (like __cleanup__) combo.
- FrozenVoid 6y agoGCC has a similar extension for defering to end of scope, which is basically most of use cases(cleanup function is usually free or some destructor/sanity check function). https://echorand.me/site/notes/articles/c_cleanup/cleanup_attribute_c.html https://echorand.me/site/notes/articles/c_cleanup/cleanup_at...
- fanf2 6y agoIt’s annoying that the C committee does not standardize existing widely-used extensions that are supported by multiple compiler implementations.
- Asooka 6y agoThis is amazing, thanks! It's a bit different though - it executes a cleanup function when the variable goes out of scope, rather than a statement at the end of the enclosing guard block. It might be better though - it avoids some footguns that would be possible with a defer implementation.
- hsaliak 6y agoClang has this too. Libraries like Glib and software like Systemd use this mechanism. Standardizing it would be nice.
- sesuximo 6y agoA nice alternative is to wrap malloc and give it a context argument. Then free via the context. This is really the same idea but doesn’t need any extra stuff in the language Or you know... free the things you need to free and move on with your life
- coldtea 6y ago>Or you know... free the things you need to free and move on with your life And leave memory leaks, buffer overflows, and bugs in the process, and we've done for the past 40+ years...
- sesuximo 6y agoSure. But defer just changes the problem from forgetting to write free to forgetting to write defer. So we’re not really talking about provably correct solutions... just things that can work. And I think a context or just manually freeing can work nicely.
- coldtea 6y ago>Sure. But defer just changes the problem from forgetting to write free to forgetting to write defer. Yes, which is a much better formulation.
- crznp 6y agoI agree that it is better when it can be used, but I don't think it can always be used (eg: long lived memory, resources with overlapping but distinct lifetimes, legacy code). So it adds a new way to misinterpret the code: is cleanup deferred or not?
- loup-vaillant 6y ago> defer just changes the problem from forgetting to write free to forgetting to write defer. That's the case only the first time you write code: { foo *f = new_foo(); // step 1 /* lots of code */ // step 3 free(f); // step 2 } vs { foo *f = new_foo(); // step 1 defer free(f); // step 2 /* lots of code */ // step 3 } Sure, in both cases you can forget step 2. But what about review? With defer, the init and cleanup code are besides each other, and a missing defer would be immediately suspicious. Without defer, you'd have to check the end of the block to make sure the cleanup code is there. The absence of the cleanup code wouldn't jump to your eyes the same way the absence of defer would. In the long run, this makes defer significantly harder to forget. --- Another significant advantage of defer is that it can handle several exit points. Imagine this code: Foo f = new_foo(); defer free(f); if (!f) { return FAIL_FOO; } Bar b = new_bar(); defer free(b); if (!b) { return FAIL_BAR; } Baz z = new_baz(); defer free(z); if (!z) { return FAIL_BAZ; } /* business logic */ /* business logic */ /* business logic */ return SUCCESS; Now the same, without defer: Foo f = new_foo(); if (!f) { free(f); return FAIL_FOO; } Bar b = new_bar(); if (!b) { free(f); free(b); return FAIL_BAR; } Baz z = new_baz(); if (!z) { free(f); free(b); free(z); return FAIL_BAZ; } /* business logic */ /* business logic */ /* business logic */ free(f); free(b); free(z); return SUCCESS; You really don't want to repeat yourself like that, you'd be liable to forget something. Now we could use `goto` and a return value: ReturnValue retval = SUCCESS; Foo f = new_foo(); if (!f) { retval = FAIL_FOO; goto cleanup; } Bar b = new_bar(); if (!b) { retval FAIL_BAR; goto cleanup; } Baz z = new_baz(); if (!z) { retval FAIL_BAZ; goto cleanup; } /* business logic */ /* business logic */ /* business logic */ cleanup: free(f); free(b); free(z); return retval; Better, except maybe the fact that Q/A hates you. All is not lost, you can still please them with a single exit point (pattern seen in the real world): ReturnValue retval = SUCCESS; Foo f = new_foo(); if (f) { Bar b = new_bar(); if (b) { Baz z = new_baz(); if (z) { /* business logic */ /* business logic */ /* business logic */ } else { retval FAIL_BAZ; } free(z); } else { retval FAIL_BAR; } free(b); } else { retval = FAIL_FOO; } free(f); return retval; To be honest this may be the worst of them all. --- The only real contenders for this use case are defer and goto, and even then I think I prefer defer.
- rwmj 6y agoPlease just standardize the already existing attribute((cleanup)) mechanism which is already being used by lots of Linux software. This new mechanism is incompatible while bringing no benefits.
- wbl 6y agoDefer has the benefit of being dynamic vs the scope tied cleanup.
- eqvinox 6y agoNot sure I'd call that a benefit, honestly.
- mikepurvis 6y agoI think it depends what problem you're trying to solve. If you're just trying to kill kernel-style GOTO cleanup pyramids, then having it tied to scope with no unwinding or anything dynamic is perfectly adequate.
- ludocode 6y agoI would tend to agree with you. Unfortunately this proposal is for much more than just block scope cleanup. This proposal contains a specification for complete stack unwinding in C. It doesn't just specify defer, but also panic and recover, which jumps between functions and cleans up guard blocks across stack frames. It's essentially exceptions for C. This is frankly horrifying and I can't believe the C standards committee is entertaining this. They claim that this is a separable feature from defer, so maybe they intend for defer to be mandatory and panic/recover to be optional. But much of the design of defer is to support their panic/recover mechanism. This makes it much more complicated than attribute((cleanup)). If they want defer to be taken seriously, they should move all of the panic/recover stuff to a separate proposal. I suspect if they did this, a lot of the design recommendations they've made for defer wouldn't make sense on their own.
- eqvinox 6y agoDoing other things in the proposal is no argument against having an attribute((cleanup)) compatible syntax for the part that overlaps. Also, the defer syntax they're proposing seems nicer in terms of syntactic sugar (no need for a stub function), but at the same time the semantics seem ... weird. Having it tied to a variable makes more sense, and sidesteps a whole bunch of weird situations (like loops with defer statements).
- anonunivgrad 6y agoThe best way to think about defer is that all the deferred statements are put in a goto label at the end of the block. When you break/return/whatever, it just jumps to that label first. This just makes slightly more convenient what the standard approach to error handling in C systems programming already is. Looking at the article, this specific implementation doesn’t quite go all the way, requiring a special guard {} block instead of working anywhere. A better implementation, working in any context, would be easy but has to be baked into the compiler. Edit: just saw the proposal for inclusion into the C standard. It is unfortunate that they are considering 1) requiring a guard block and 2) deferring clean up to the end of the function instead of end of the scope. #1 is needless syntax and #2 would make the feature useless within loops by causing explosions and contractions in memory usage.
- ufo 6y agoReading the PDF, I got the impression that the main thing in favor of using a guard keyword is that it would allow it to be implemented as a library. That way it wouldn't require changes to compilers and it might also be possible to adopt into the C++ standard. However, the version without the guard keyword is arguably more ergonomic.
- derriz 6y agoHow can you get away from having an explicit guard block? Without one, you couldn't 'defer' to the end of a containing block from within an if/while/do/for/etc. block. When/where the deferred code is executed has to be specified some way so an explicit marker for the defer 'scope' is surely required without severely limiting the utility of the feature.
- anonunivgrad 6y agoYou would just follow the C variable scope rules I think. But I see now how this creates an issue if you want to put the defer, but not the acquisition of the resource it releases, within a control statement.
- ufo 6y agoStarting in page 16, there is a section called "Do we want a guard keyword?" that talks about this. They give an example showing that deferring to the end of the containing block is possible without the guard keyword, although it's a bit wonky, requiring to duplicate the "if". With guard keyword: guard { void *ptr = malloc(12); if (ptr) { defer free(ptr); // Use ptr } // Use ptr some more } // free ptr here Without guard keyword: { void *ptr = malloc(12); defer { if (ptr) free(ptr); } if (ptr) { // Use ptr } // Use ptr some more } // free ptr here
- apples_oranges 6y agoWhen I first saw defer I thought I'd use it much more. With Rx-type programming it's kind of even more obsolete no?
- loup-vaillant 6y agoWhat's "Rx-type programming"?
- jcelerier 6y agodefining flow graphs in code, see examples here: https://github.com/ReactiveX/RxJava https://github.com/ReactiveX/RxJava
- kzrdude 6y agoWhy is there a special guard keyword? Scope limits are already clear.
- ufo 6y ago> The guard statement allows for a library implementation. Foregoing the possibility of a library implementation, a possible design choice could be to eliminate the guard statement as it would eliminate the need for an additional reserved keyword and the requirement for programmers to create guarded blocks around deferred statements. If the guard statement is not used, the proposed changes to the behavior of the break statement would likely be eliminated as well (see Appendix K).
- apankrat 6y agoRe: Should object values be captured? The results from 387 responses (to a Twitter poll) show a 2:1 preference for the value being read at the time the deferred statements are executed (66.9%) rather than when the defer statement encountered(33.1%). Since both options - by value and by reference - may be viewed as reasonable or desirable, neither should be a default. Instead, they both should be using a special syntax. So if you write something likes this - guard { void * p = malloc(...); defer free(p); } it simply won't compile. Instead you'd need to say something like this - guard { void * p = malloc(...); defer free(^p); // evaluate now } guard { void * p = malloc(...); defer free(p^); // evaluate later } This may also be reused later for specifying lambda captures... should lambdas ever make their way into C.
- flohofwoe 6y agoThe ^ is already taken in context of "C lambdas" though (hoping that if C ever gets lambdas it will simply adopt clang blocks instead of a C++ like syntax): https://clang.llvm.org/docs/BlockLanguageSpec.html https://clang.llvm.org/docs/BlockLanguageSpec.html
- apankrat 6y agoAye, I've seen that. IMO it would've been better (read, cleaner) to merge lambdas and function pointers into a single language construct. Throw in the partial application too and we'd be have a natively supported concept of a "callable" instance - void foo(int tick); void bar(int tick, int tock); void do_something( void (* progress)(int tick) ); do_something( foo ); do_something( bar(,1) ); do_something( void (int tick) { /* lambda */ } ); The syntax is approximate, but for the code that is using callback-based flows this would've been very handy.
- gpderetta 6y agoproblem is, to unify lambdas and function pointers you either break the ABI and use fat pointers or you need to disable W^X which is a security issue.
- mskslal 6y agoIn its current form, this is really stupid, because of something like this: guard { for (int i = 0; i < n; i++) { defer foo(i); } } Now the compiler has to: 1. implement some side of capture/closure mechanism to keep all the 'i's to the end of the guard block 2. do dynamic allocation to store the closures so they can be executed at the end did the scope 1 seems like too much work for such a feature, and 2 is a massive no. Implicit dynamic allocation, in C? And all of this for nothing. The guard syntax doesn't give any reasonable benefits. They should have just kept it simple; defer happens at the end of the scope, and it just takes 'i' by "reference". It's a shame because it's a feature I would really like to have.
- ufo 6y agoI got the impression that the proposal still has some aspects of the design that are open for discussion, including whether it should be static or dynamic, and whether it should capture the variables by value or by reference. (These are discussed in pages 13-16)
- scott_s 6y agoYes, see the discussion in the proposal on page 13, under the heading "Should defer statements be static or dynamic?": http://www.open-std.org/jtc1/sc22/wg14/www/docs/n2542.pdf http://www.open-std.org/jtc1/sc22/wg14/www/docs/n2542.pdf
- rseacord 6y agoBased on feedback from the committee, I think it's very unlikely a dynamic approach will be used. The static only affects the for loop in terms of whether the deferred statements are run once or for each iteration of the loop, and the only real world use of this I've found so far involves allocating and deallocating resources within each loop iteration which would be supported by the static approach.
- huhtenberg 6y agoThey can avoid #2 by doing by-reference capture or they can piggy-back on alloca(). Still, I agree that this is not "nice and clean". Ultimately it doesn't feel like something that belongs in C.
- earthboundkid 6y agoZig has defer and errdefer. C should just steal however Zig does it.
- 0xTJ 6y agoI love Zig, and I'm looking forward to it continuing to mature. I've been waiting for a specific compiler TODO to be resolved (something like initializing union bitfields) that (at least as of 0.7.0) wasn't yet implemented, because I was using it in a toy OS for some GDT stuff.
- dnautics 6y agoHad some difficulty with toy os gdt too! I think what you are looking for is the packed union bug. I would suggest doing it "the hard way", one thought would be treating it as an array (comptime known length) of u8
- pjmlp 6y agoA very welcomed addition.
- quelsolaar 6y agovoid * * a = NULL; while(TRUE) { a = malloc(sizeof *a); if(a == NULL) break; *a = malloc(1024); if(*a == NULL) break; if(!do_some_other_tests(a)) break; return a; } /* cleanup / if(a == NULL) return; if(a != NULL) free(*a); free(a); alt: void * * a = NULL; switch(TRUE) { defaultt : a = malloc(sizeof *a); if(a == NULL) break; *a = malloc(1024); if(*a == NULL) break; if(!do_some_other_tests(a)) break; return a; } /* cleanup / if(a == NULL) return; if(a != NULL) free(*a); free(a);
- grok22 6y agoIf the proposed implementation requires a guard clause, then it seems like it is no better than the while (TRUE) implementation above and it's not worth the support in the standard. Maybe it's a bit more structured and covers a few more use-cases, but not worth the standardization.
- gusttedt 6y agoIn the current proposal, the whole function body can act as a guarded block. So for simple cleanup strategies that are bound to functions, you wouldn't need this.
- DudeInBasement 6y agoJust use some macros, easy peasy. (gcc computed goto) #define DEFER_START(scope_name) void * scope_name = &&scope_name##_end; #define DEFER(scope_name, iter_name, func) void * iter_name = scope_name; { \ scope_name = &&iter_name ##_exe; \ if (0) { iter_name##_exe: {func} goto *iter_name; } } #define DEFER_END(scope_name) scope_name##_start: goto *scope_name; scope_name##_end: {} #define DEFER_EXE(scope_name) goto scope_name##_start; ... { DEFER_START(scope); void * p = malloc(4); if (!p) DEFER_EXE(scope); DEFER(scope, free_p, { printf("freep\n"); free(p); }); void * q = malloc(40); if (!q) DEFER_EXE(scope); DEFER(scope, free_q, { printf("freeq\n"); free(q); }); DEFER_END(scope); } you can even macro the free_p/ free_q names with line numbers to shorten the macros, IE DEFER(scope, {});
- DudeInBasement 6y agoAdd: If you want an easier, and use shadowed variables: #define guard_name3(name, line) name ## line #define guard_name2(name, line) guard_name3(name, line) #define guard_line_name(name) guard_name2(name, __LINE__) #define guard { void * defer_scope = &&guard_line_name(defer_scope ##_exe); \ if (0) { guard_line_name(defer_scope ##_exe) : defer_scope = 0; }\ if (defer_scope != 0) #define guard_end goto * defer_scope; } #define guard_break goto *defer_scope; #define defer(func) void * guard_line_name(defer_scope_item) = defer_scope; \ defer_scope = &&guard_line_name(defer_scope_item##_label); \ if (0) { guard_line_name(defer_scope_item##_label): { func } goto * guard_line_name(defer_scope_item); } guard { void * p = malloc(10); if (!p) guard_break; defer({ printf("freep\n"); free(p);}); void * q = malloc(10); if (!q) guard_break; defer({printf("freeq\n"); free(q);}); void * fail = 0; if (!fail) guard_break; defer({printf("freefail\n"); free(fail);}); guard_end; }
- nrclark 6y agoI think it's great that the authors are working on this. RAII is one of the best things about C++, and I'm excited for similar functionality in C. GCC's __cleanup__ is a poor substitute for a fully-baked addition to the language. From skimming through the paper, it looks like there's an open discussion about the 'guard' keyword and scoping. I know that the scoping rules are tricky. Would it make sense for defer statements to be attached to a variable's scope instead of a scope block? It would look something like GCC's __cleanup__, except that it could run an arbitrary statement/block instead of a callback. Scope-level defer() could be specified by attaching to a depth number. If anybody involved in the paper is reading this, what would you think about this syntax? //---------- Attaching a defer() to a variable's scope -----------// int main(void) { int *dummy = malloc(sizeof(int)); defer (dummy) { printf("This statement prints second.\n"); free(dummy); } printf("This statement prints first.\n"); } //------- Attaching a defer() to the current block's scope -------// int main(void) { int *dummy; do { dummy = malloc(sizeof(int)); defer (0) { printf("This statement prints second.\n"); free(dummy); } printf("This statement prints first.\n"); } while (0); printf("This statement prints third.\n"); } //-------- Attaching a defer() to a parent block's scope ---------// int main(void) { int *dummy; do { do { dummy = malloc(sizeof(int)); defer (1) { printf("This statement prints third.\n"); free(dummy); } printf("This statement prints first.\n"); } while (0); printf("This statement prints second.\n"); } while (0); printf("This statement prints fourth.\n"); } This syntax would eliminate the need for an explicit guard keyword, and would also make for a straightforward porting process for all code that currently uses __cleanup__. It also feels a little more C-like to me, in that it resembles the look-and-feel of other control-flow statements. In my example, defer() with a variable-name would attach itself to the variable's scope, and would execute when the variable leaves scope. And defer() with an integer would attach itself to [current_scope_level - target_value]. So a defer(0) would trigger at the end of the current block scope, and defer(1) would trigger at the end of the parent block scope. Combined with generic {} blocks, you could get the same behavior provided by the paper's suggested guard keyword.
- david2ndaccount 6y agoThe panic/recover mechanism would be a disaster. C code is not written with the possibility of stack unwinding in mind and this introduces the possibility of non-visible, non-local control flow at every function call. The simple possibility of that would massively hurt codegen. One of the benefits of writing in C over C++ is that you don’t have to worry about non-local control flow like exceptions.
- hhas01 6y ago“What we are trying to accomplish…” See also: silk purse, sow’s ear.
- dang 6y agoWe detached this comment from https://news.ycombinator.com/item?id=25420221 https://news.ycombinator.com/item?id=25420221. Please don't post shallow or snarky comments to HN [1]. It is particularly damaging when talking to a genuine expert [2]. Having a user like rseacord commenting in threads like this is great for everybody. When you incentivize them to leave, you harm the entire community. Your other comment [3] was pretty insulting too (at the beginning); people don't serve on standards committees because of money. [1] https://news.ycombinator.com/newsguidelines.html https://news.ycombinator.com/newsguidelines.html [2] https://news.ycombinator.com/item?id=22865357 https://news.ycombinator.com/item?id=22865357 [3] https://news.ycombinator.com/item?id=25420638 https://news.ycombinator.com/item?id=25420638 Edit: we've already had to ask you to stop posting in the flamewar style to HN, and you've been doing it repeatedly lately. Would you please review the guidelines and use HN as intended? I don't want to ban you.
- drran 6y agoMy version of defer() for C is just one line. https://github.com/vlisivka/crust/blob/master/crust-mem.h#L37 https://github.com/vlisivka/crust/blob/master/crust-mem.h#L3... (Warning: GPL3) I switched to Rust and lost interest in C.
- saagarjha 6y agoYou can't really claim a license on one line of code that shows off a GCC extension :P
- kazinator 6y agoThe astonishing thing here is that there is a forum where papers on C hacks can still be considered for publication. I some ways that is encouraging, because research in practical systems things is valuable and used to be one of the cornerstones of computer science.
- worik 6y agoIf you want these sort of services, why use C?
- ironmagma 6y agoSometimes you have to use C because there are no other options, especially for embedded systems which have no (e.g.) LLVM support.
- sorisos 6y agoThere is a lot of legacy C code out there.
- pjmlp 6y agoUNIX clones are not going to get rewritten into something else, so we need to improve it.
- liquidify 6y agoI can't say anything about whether this is a good idea in general or not, but if they are going to do it, then why not just combine the allocation with the defered free into a single syntax ... 'void* x = defer_free_malloc (...);'
- unwind 6y agoI came to say this too, it seemed like an obvious evolution. I find the "randomness" of the defer-statement a bit scary, like there is too much freedom. There's no guarantee that you're deferring the proper code to free the resource, which somehow should be the default. Not at all sure of the proper syntax, but at least tying it together with something like void * const x = DEFER(malloc(...), free); would make sense. In a more high-level language with interfaces, there should be a way to tie together the creation and destruction into a combined type, and just do obj = acquire SomeType(...); And then 'acquire' should take care that the type to the right implements the interface, and automatically add a deferred call to the proper freeing method. Hm. I guess I just invented some kind of garbage collection, bummer.
- deleted 6y ago[deleted]
- cyber1 6y agoThis is super cool! I want this!
- gusttedt 6y agoJust use the reference implementation that is linked in the post. We would much appreciate feedback from users at this point.
- sorisos 6y agoThis feature have the potential to remove some christmas trees (nested if statements) in MISRA C compatible code. Publishers should have waited to January! :)
- tpoacher 6y agoWhat does this offer that couldn't be accomplished with well placed goto's like in this example https://vilimpoc.org/research/raii-in-c/ https://vilimpoc.org/research/raii-in-c/ (yes I get that defer > raii, but the question is meant to be more general)
- tpoacher 6y agohm, ok, they address this in the paper. "This code [i.e. goto-error-handling] has the advantage of making the conditional error handling code explicit but at the cost of proximity; the cleanup code is removed from where its need arises. This linearization requires a naming convention for the labels [...]"
- childintime 6y agoHow sensible this maybe, I don't feel like improving C, at all. It would be much more beneficial to bring C into the Rust cargo infrastructure, such that mixed C and Rust projects can exist with minimal friction. I just did a search and it seems https://crates.io/crates/cc https://crates.io/crates/cc comes close. If only an IDE could do the job automatically when it finds .c files in the project. Even better, it should find and install a local compiler when needed (C or any other).