[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)