[HN Gopher] C: Simple Defer, Ready to Use
       ___________________________________________________________________
        
       C: Simple Defer, Ready to Use
        
       Author : ingve
       Score  : 109 points
       Date   : 2025-01-06 18:36 UTC (4 hours ago)
        
 (HTM) web link (gustedt.wordpress.com)
 (TXT) w3m dump (gustedt.wordpress.com)
        
       | stefanos82 wrote:
       | This should have been in C ages ago, but better late than never!
        
         | cmovq wrote:
         | This isn't new. The only C23 feature this article uses is the
         | attribute syntax, but you could just use
         | `__attribute((cleanup(F)))` instead.
        
           | robertlagrant wrote:
           | Tis a joke my friend.
        
             | marmaduke wrote:
             | i like how the contraction it's and abbreviation T'is are
             | anagrams
        
               | sigzero wrote:
               | It is not T'is, it is Tis. No apostrophe.
        
               | hollerith wrote:
               | I've only ever seen it spelled 'tis
        
               | dkjaudyeqooe wrote:
               | It's a contraction of "it is" so " 'tis " is correct.
        
               | jcranmer wrote:
               | The apostrophe is in the wrong spot, but 'tis is the
               | correct spelling, and the only one I've ever seen.
        
               | IncRnd wrote:
               | The word is 'tis with an apostrophe.
               | 
               | https://www.britannica.com/dictionary/%27tis
               | 
               | https://www.merriam-webster.com/dictionary/%27tis
               | 
               | https://dictionary.cambridge.org/us/dictionary/english/ti
               | s
        
       | webstrand wrote:
       | Very nice, using the goto trick to perform cleanups always felt
       | dirty to me.
        
         | foooorsyth wrote:
         | gotos can be used without shame. Dijkstra was wrong (in this
         | rare case).
         | 
         | defer is cleaner, though.
        
           | jerf wrote:
           | Dijkstra was not wrong. Modern programmers are wrong in
           | thinking that the goto that they use is what Dijkstra was
           | talking about, merely because of the fact it happens to be
           | called the same thing. I mean, I get how that can happen, no
           | sarcasm, but the goto Dijkstra was talking about and what is
           | in a modern language is not the same.
           | https://jerf.org/iri/post/2024/goto/
           | 
           | The goto Dijkstra is talking about is dead. It lives only in
           | assembler. Even BASIC doesn't have it anymore in any modern
           | variant. Modern programmers do not need to live in fear of
           | modern goto because of how awful it was literally 50 years
           | ago.
        
             | Spivak wrote:
             | Or the tl;dr in modern parlance Dijkstra was railing
             | against the evils of setjmp.
        
               | ryao wrote:
               | Lua uses it for error handling. It is really hard to
               | understand the lua code. :/
        
               | zzo38computer wrote:
               | Sometimes setjmp is useful and I had occasionally used
               | it, but usually it is not needed. There is certain
               | considerations you must make in order to be careful when
               | you are using setjmp though.
               | 
               | (Free Hero Mesh uses setjmp in the execute_turn function.
               | This function will call several other functions some of
               | which are recursive, and sometimes an error occurs or
               | WinLevel or LoseLevel occurs (even though these aren't
               | errors), in which case it will have to return
               | immediately. I did try to ensure that this use will not
               | result in memory leaks or other problems; e.g.
               | v_set_popup allocates a string and will not call longjmp
               | while the string is allocated, until it has been assigned
               | to a global variable (in which case the cleanup functions
               | will handle this). Furthermore, the error messages are
               | always static, so it is not necessary to handle the
               | memory management of that either.)
        
               | kbolino wrote:
               | No, even setjmp/longjmp are not as powerful or dangerous.
               | The issue is not the locality of the jump, but the lack
               | of any meaningful code structure enforced by the
               | language. Using setjmp and longjmp _properly_ still saves
               | and restores context. You still have a function call
               | stack, a convention for saving and restoring registers,
               | locally scoped variables, etc. Though, using setjmp
               | /longjmp _improperly_ on some platforms might come close,
               | since you 're well into undefined behavior territory.
               | 
               | Parent is correct that this doesn't really exist outside
               | of assembly language anymore. There is no modern
               | analogue, because Dijkstra's critique was so successful.
        
             | readthenotes1 wrote:
             | "when I see modern code that uses goto, I actually find
             | that to be a marker that it was probably written by highly
             | skilled programmers. "
             | 
             | He should have said "correct code", not "modern code"
             | because the times I remember seeing goto the code was
             | horribly incorrect and unclear.
             | 
             | (With break and continue, someone has to be doing something
             | extra funky to need goto. And even those were trigger signs
             | to me, as often they were added as Hail Mary's to try to
             | make something work)
             | 
             | {I typically reviewed for clarity, correctness, and
             | consistency. In that order}
        
             | huhtenberg wrote:
             | The interpretation of Dijkstra's sentiment in your blog
             | post is plain wrong.
             | 
             | His paper [1] clearly talks about goto semantics that are
             | still present in modern languages and not _just_
             | unrestricted jmp instructions (that may take you from one
             | function into the middle of another or some such). I 'd
             | urge everyone to give it a skim, it's very short and on
             | point.
             | 
             | [1] https://homepages.cwi.nl/~storm/teaching/reader/Dijkstr
             | a68.p...
        
             | jcranmer wrote:
             | Your analysis is wrong. Dijkstra was a big proponent of
             | structured programming, and the fundamental thesis of his
             | argument is that the regular control flow structures we're
             | used to--if statements, loops, etc.--all represent a tree-
             | based data structure. In essence, the core argument is that
             | structured programming allows you to mentally replace large
             | blocks of code with black boxes whose exact meanings may
             | not be important.
             | 
             | The problem with GOTO, to Dijkstra, is that it violates
             | that principle. A block can arbitrarily go somewhere else--
             | in the same function, in a different function (which
             | doesn't exist so much anymore)--and that makes it hard to
             | reason about. Banning GOTO means you get the fully
             | structured program that he needs.
             | 
             | (It's also worth remembering here that Dijkstra was writing
             | in an era where describing algorithms via flowcharts was
             | common place, and the use of if statements or loops was far
             | from universal. In essence, this makes a lot of analysis of
             | his letter difficult, because modern programmers just
             | aren't exposed to the kind of code that Dijkstra was
             | complaining about.)
             | 
             | Since that letter, modern programming _has_ embraced the
             | basic structured programming model--we think of code almost
             | exclusively of if statements and loops. And, in many
             | language, goto exists only in extremely restricted forms
             | (break, continue, and return as anything other than the
             | last statement of a function). It should be noted that
             | Dijkstra 's argument actually carries through to railing
             | against the modern versions of break et al, but the general
             | program of structured programming has accepted that "early
             | return" is an acceptable deviation from the strictly-
             | single-entry-single-exit that is desired that doesn't
             | produce undue cognitive overhead. Even where mind-numbing
             | goto exists today (e.g., C), it's similarly largely used in
             | ways that are similar to "early return"-like concepts, not
             | the flowchart-transcribed-to-code-with-goto-as-sole-
             | control-flow style that Dijkstra is talking about.
             | 
             | And, personally, when I work with assembly or LLVM IR
             | (which is really just portable assembly), I find that the
             | number one thing I want to look at a very large listing is
             | just something that converts all the
             | conditional/unconditional jumps into if statements and
             | loops. That's really the main useful thing I want from a
             | decompiler; everything else as often as not just turns out
             | to be more annoying to work with than the original
             | assembly.
        
         | ryao wrote:
         | I really like the goto trick. It makes functions readable,
         | versus the mess you have without it.
        
         | cjensen wrote:
         | I use a few strategies instead of goto:
         | 
         | (1) For simpler cases, wrap in do {} while (0) and break from
         | the loop
         | 
         | (2) For multiple cleanups, use same technique combined with
         | checks to see if the cleanup is required. E.g. if (f != null)
         | fclose (f)
         | 
         | (3) put the rest of the stuff in another function so that the
         | exit code must run on the way out.
         | 
         | In 35 years of coding C/C++, I've literally never resorted to
         | goto. While convenient, this new defer command looks like the
         | kind of accidental complexity that templates brought to C++.
         | That is, it provides a simple feature meant to be solve simple
         | problems in a simple way that accidentally allows architecture
         | astronauts the ability to build elaborate footguns.
        
           | dktoao wrote:
           | `the do {} while (0)` block with breaks does exactly what
           | goto does but it is so much more hacky, less flexible and
           | harder to follow IMHO.
        
           | armitron wrote:
           | 35 years of coding C don't amount to much if you ignore best
           | practices. Look at the Linux kernel to learn how gotos can be
           | used to make the code safer and improve clarity.
           | 
           | There's probably something wrong if a substantial project in
           | C does NOT use gotos.
        
       | loeg wrote:
       | Do defers get processed if an exception is thrown? I know C
       | doesn't have exceptions, but real world C code often interacts
       | with C++ code. So it is necessary imo to define that interaction.
       | 
       | In C++ we have something pretty similar already in the form of
       | Folly ScopeGuard (SCOPE_EXIT {}).
        
         | kevin_thibedeau wrote:
         | GCC has the __cleanup__ attribute which works in conjunction
         | with exceptions in C to provide an RAII mechanism.
        
           | eqvinox wrote:
           | No it doesn't; C functions don't set up appropriate
           | registrations with the C++ exception / stack unwinding
           | systems, thus C functions are simply skipped over on the way
           | up towards the nearest exception handler.
           | 
           | __attribute__((cleanup(...))) is purely a scope-local
           | mechanism, it has absolutely nothing to do with exceptions.
        
             | leni536 wrote:
             | _" If -fexceptions is enabled, then cleanup_function is run
             | during the stack unwinding that happens during the
             | processing of the exception. Note that the cleanup
             | attribute does not allow the exception to be caught, only
             | to perform an action. It is undefined what happens if
             | cleanup_function does not return normally."_[1]
             | 
             | While it's true that it -fexceptions is disabled for C by
             | default, some C libraries need to enable it anyway if they
             | want to interact with C++ exceptions this way. For example
             | C++ requires that qsort and bsearch propagate the exception
             | thrown by the comparison callback normally, so libc
             | implementations that are also used from C++ do enable it.
             | 
             | [1] https://gcc.gnu.org/onlinedocs/gcc/Common-Variable-
             | Attribute...
        
             | kevin_thibedeau wrote:
             | GCC has comon exception handling across its supported
             | languages. They have exceptions as a C extension that
             | interoperate with C++. Cleanups get called during a stack
             | unwind because they are considered the same as a
             | destructor.
        
         | ryao wrote:
         | I am reminded of this:
         | 
         | https://news.ycombinator.com/item?id=42532979
         | 
         | Interactions with stack unwinding are not considered by C, so I
         | doubt this would be. Compilers would be free to make it work,
         | however. This is just my guess.
        
         | cryptonector wrote:
         | `extern "C"` functions coded in C++ should really not throw! I
         | would say that they MUST NOT throw.
        
       | andrewmcwatters wrote:
       | I know you can do these things, but I've always tried to avoid
       | macro programming all together in C and C++ unless forced to use
       | them by dependencies in idiomatic usage.
        
       | accelbred wrote:
       | Unfortunately, this requires an executable stack, and only works
       | on gcc, though an alternate implementation can work on clang.
       | 
       | After a few attempts at defer, I ended up using a cleanup macro
       | that just takes a function and a value to pass to it:
       | https://github.com/aws-greengrass/aws-greengrass-lite/blob/8...
       | 
       | Since the attribute or a function-like macro in the attribute
       | position broke the c parsing in some tooling, I made the macro
       | look like a statement.
        
         | uecker wrote:
         | It does not need an executable stack:
         | https://godbolt.org/z/K1GTa4jh4
        
           | accelbred wrote:
           | The macro from the article uses nested functions which on gcc
           | are implemented with tramolines that need executable stack.
           | 
           | https://thephd.dev/lambdas-nested-functions-block-
           | expression...
        
             | uecker wrote:
             | The trampolines are not generated in this case.
        
               | eqvinox wrote:
               | To be fair you're not communicating very well -- there
               | are no trampolines being used here because the call site
               | is directly in the same function, so no trampoline is
               | needed with already being in the correct stack frame.
               | 
               | (And also on -O1 and higher the entire call is optimized
               | out and the nested function inlined instead.)
        
               | uecker wrote:
               | Fair, I was not explaining why the statement was wrong.
               | Anyhow, the trampoline is not related to the call site
               | being in the same function but whether a pointer is
               | generated to the nested function and escapes. Nested
               | functions in general do not need trampolines or
               | executable stack.
        
               | accelbred wrote:
               | Nice, if this is reliable across gcc versions and
               | optimization levels, I might consider it for future
               | stuff. Though making it such that treesitter and other
               | tools dont barf on it would still need investigation.
        
               | Gibbon1 wrote:
               | I've used nested functions for a very long time with gcc
               | and they are completely reliable. Since my code is
               | embedded without an MMU the oh noes executable stack
               | doesn't fill me with any dread.
               | 
               | It's unfortunate a lot of the standards guys are
               | horrified by anything that isn't C89. Because if the
               | executable stack is an issue it's worth fixing.
               | 
               | Side note: 20 years ago people thought if they made the
               | stack non executable that would totally fix stack
               | smashing attacks and unfortunately it only slows down
               | script kiddies.
        
               | uecker wrote:
               | And I should mention that GCC nowadays can also allocate
               | the trampoline on the heap: -ftrampoline-impl=heap
        
       | Night_Thastus wrote:
       | I really, really do not like what that could do to the
       | readability of code if it was used liberally.
       | 
       | It's like GOTOs, but worse, because it's not as visible.
       | 
       | C++'s destructors feel like a better/more explicit way to handle
       | these sorts of problems.
        
         | skinner927 wrote:
         | But thats C++
        
           | Night_Thastus wrote:
           | I am aware. I'm just pointing it out because they're similar
           | ideas.
        
         | accelbred wrote:
         | Destructors are hidden control flow, and can be non-obvious for
         | structs from other files. I find they make code significantly
         | harder to follow than in plain C. Defer does not have the
         | problem, as all the logic in your function is explicitly there.
        
           | quelsolaar wrote:
           | Not right there, some other place in the function. Also
           | people will start adding defer in macros and then things will
           | go sideways.
        
         | Analemma_ wrote:
         | I don't mind destructors but I don't understand your insistence
         | that they're significantly better than defer for visibility:
         | they're invisible and invoke a distant jump to some bit of code
         | which might not even be in your codebase; defer is "right
         | there" (and with proper editor support could even be made to
         | look like it happens at the end of the function instead of
         | where it's declared). I think they're both fine for their
         | respective languages.
        
         | cryptonector wrote:
         | > I really, really do not like what that could do to the
         | readability of code if it was used liberally.
         | 
         | > It's like GOTOs, but worse, because it's not as visible.
         | 
         | > C++'s destructors feel like a better/more explicit way to
         | handle these sorts of problems.
         | 
         | But what C++ gives you is the same thing:
         | 
         | > It's like GOTOs, but worse, because it's not as visible.
         | 
         | !
         | 
         | The whole point of syntactic sugar is for the machinery to be
         | hidden, and generated assembly will generally look like goto
         | spaghetti even when your code doesn't.
         | 
         | What this implementation of defer does under the covers is not
         | interesting unless you're trying to make it portable (e.g., to
         | older MSVC versions that don't even support C99 let alone C23
         | or GCC local function extensions) or efficient (if this one
         | isn't).
        
         | kubb wrote:
         | How do you write C code that needs to do this (set up several
         | resources, and clean only some of them up depending on where
         | the function returns) so that it's easy to follow?
        
         | scott_s wrote:
         | Look at the Linux kernel. It uses gotos for exactly this
         | purpose, and it's some of the cleanest C code you'll ever read.
         | 
         | C++ destructors are great for this, but are not possible in C.
         | Destructors require an object model that C does not have.
        
       | tuveson wrote:
       | For parity, C should add a `prefer` keyword that hoists
       | statements to the top of the function.
        
         | rurban wrote:
         | No, C is not perl. They do have BEGIN blocks, but these are
         | constexpr.
        
       | fuhsnn wrote:
       | The C++ lambda version can be a foot gun:
       | https://godbolt.org/z/Wd66GcrdG, if the return value is a struct,
       | NRVO (named return value optimization) may be applied and the
       | lambda will be called in different order.
       | 
       | As for the n3434 proposal, given that the listed implementation
       | experiences are all macro-based, wouldn't it be more easily
       | adopted if proposed as a standard macro like <stdarg.h>?
        
       | worik wrote:
       | Does C really need this?
       | 
       | Do languages need to grow in this way?
       | 
       | The overriding virtue of C is simplicity.
        
         | vkazanov wrote:
         | Yes it does, and numerous projects already use variants of this
         | trick.
        
           | fuhsnn wrote:
           | Including Linux kernel https://github.com/torvalds/linux/blob
           | /master/include/linux/...
        
         | pjmlp wrote:
         | That was lost long time ago, after K&R C became part of WG14.
        
       | eqvinox wrote:
       | Unfortunately, nested functions are one of the few GCC extensions
       | not implemented by clang, as such this really only works on GCC.
        
         | loeg wrote:
         | If this proposal is adopted in C2Y, surely Clang will implement
         | it.
        
           | pjmlp wrote:
           | Most likely yes, although being on ISO doesn't mean much,
           | there are plenty of examples of features that not every
           | compiler fully supports.
        
       | bangaladore wrote:
       | I'm a strong believer that if C had defer, many bugs would
       | disappear. The number 1 issue I find myself having when switching
       | from C++ to C is missing RAII (Main C++ way of implementing
       | defer).
        
         | zwieback wrote:
         | I agree about missing RAII when switching away from C++ but it
         | seems like defer cleans up after enclosing function exits while
         | RAII cleans up when object goes out of scope, which is more
         | fine-grained. Maybe I'm misunderstanding exactly when defer
         | would clean up but it seems more like a safety feature. As
         | people pile more stuff into the function the cleanup would be
         | deferred more and more while RAII encapsulates the management
         | right where you need it, e.g. exiting a loop or something.
        
           | bangaladore wrote:
           | Yeah, defer within scope is the most ideal form in my
           | opinion.
        
           | accelbred wrote:
           | This defer (using attribute cleanup) as well as Zig's are run
           | when going out of scope. Go's runs at function exit.
        
           | Arnavion wrote:
           | defer-based cleanup also has the problem that, if the
           | function plans to return a value it has to make sure to *not*
           | defer its cleanup, and the caller then has to remember to
           | clean it up. Destructor-based cleanup avoids these problems,
           | but of course it's reasonable for languages (existing ones
           | like C and even newer ones like Zig) to not want destructors,
           | so defer is the next best thing.
           | 
           | Note that C++ destructors are also not ideal because they run
           | solely based on scope. Unless RVO happens, returning a value
           | from a function involves returning a new value (created via
           | copy ctor or move ctor) and the dtor still runs on the value
           | in the function scope. If the new value was created via copy
           | ctor, that means it unnecessarily had to create a copy and
           | then destroy the original instead of just using the original.
           | If the new value was created via move ctor, that means the
           | type has to be designed in such a way that a "moved-out"
           | value is still valid to run the dtor on. It works much better
           | in Rust where moving is not only the default but also does
           | not leave "moved-out" husks behind, so your type does not
           | need to encode a "moved-out" state or implement a copy ctor
           | if it doesn't want to, and the dtor will run the fewest
           | number of times it needs to.
        
         | quelsolaar wrote:
         | ...and a we would get a host of new bugs that would be a lot
         | harder to fix. Invisible jumps are very bad.
        
           | bangaladore wrote:
           | Nearly every modern language supports defer in some sense.
           | Unless you are talking about RAII hiding the defer, but
           | that's not the case with a custom Defer type or similar that
           | takes a closure in the constructor.
           | 
           | Defer is a way better solution than having to cleanup in
           | every single failure case.
        
         | pjmlp wrote:
         | The major source of bugs in C is located on string.h, and
         | nothing has been made in 50 years to fix that.
         | 
         | Really fix, not mitigations with their own gotchas.
        
       | jagrsw wrote:
       | Ages old (~2014 IIRC) defer implementation for gcc and for clang:
       | 
       | https://github.com/google/honggfuzz/blob/c549b4c31815e170d3b...
        
       | joshmarinacci wrote:
       | Unfortunately the first thing I see is a giant ad scroll down
       | over the header blocking the entire viewport on my phone, so
       | immediately hit the back button. Are all personal blogs
       | automatically monetized now?
        
         | wgjordan wrote:
         | Free sites on WordPress.com are:
         | 
         | https://wordpress.com/support/no-ads/
        
       | senderista wrote:
       | For C++, scope_guard has been around forever.
        
       | dataflow wrote:
       | What are the unwind semantics on GCC, Clang, MSVC?
        
       | aleden wrote:
       | Nobody knows about BOOST_SCOPE_DEFER?
       | 
       | #include <boost/scope/defer.hpp>
       | 
       | BOOST_SCOPE_DEFER [&] {                   close(fd);
       | 
       | };
       | 
       | Implementation here:
       | https://github.com/boostorg/scope/blob/develop/include/boost...
        
         | bdamm wrote:
         | Only for those willing to stomach the entire boost hairball,
         | which in embedded environments might be too much to ask.
        
       | btown wrote:
       | I once worked on a student robotics project where if you didn't
       | gracefully shut down the connection to a specialized camera, it
       | had a significant chance of _physically bricking the camera._
       | 
       | We were using C++ and essentially instrumented code review
       | processes, despite being a student group, to ensure nothing was
       | ever called in a way where the destructor wouldn't be called -
       | and broke out vision processing into a separate process so if
       | other processes crashed we'd still be okay. In retrospect it was
       | amazing training for a software engineering career.
       | 
       | But I always look at the words "simple" and "defer" and shudder
       | when I see them next to each other!
       | 
       | Just like you'd have a "threat model" for cybersecurity, make
       | sure you understand the consequences of a defer/destructor
       | implementation not functioning properly.
        
       ___________________________________________________________________
       (page generated 2025-01-06 23:00 UTC)