[HN Gopher] A simple defer feature for C
       ___________________________________________________________________
        
       A simple defer feature for C
        
       Author : ingve
       Score  : 142 points
       Date   : 2022-01-16 16:11 UTC (6 hours ago)
        
 (HTM) web link (www.open-std.org)
 (TXT) w3m dump (www.open-std.org)
        
       | josephcsible wrote:
       | I don't like the nonlocality of this. When I see a closing curly
       | brace, I want to be able to tell what execution will do when it
       | gets there just by looking immediately before the corresponding
       | opening curly brace, but now I'm going to have to look in the
       | entire body of the block too. If I wanted things to be this
       | implicit and nonlocal, then I'd have chosen C++ instead of C.
        
       | yk wrote:
       | I don't like it. The great advantage of C is the very simple
       | virtual machine and therefore very easy to reason about code.
       | This bolting on language features is the hallmark of C++, which
       | don't get me wrong has advantages, but in case we want the
       | advantages we can already use C++.
        
         | eptcyka wrote:
         | The feature wouldn't introduce much complexity to the abstract
         | machine model IMO, not any more than what cleanup labels
         | already do.
        
           | mort96 wrote:
           | ..And cleanup attributes aren't in C.
        
           | yk wrote:
           | Cleanup labels are just a conditional jump, it's explicitly
           | in the code. The defer feature would introduce implicit
           | guarantees by the compiler.
        
         | dgellow wrote:
         | defer has nothing to do with C++.
        
           | dahfizz wrote:
           | C++ has try finally, which is the same thing. Instead of
           | bloating C, anyone needing defer can use a subset of C++ that
           | fits their needs.
        
             | eMSF wrote:
             | C++ has no finally blocks.
        
               | AnonC wrote:
               | > C++ has no finally blocks.
               | 
               | Not in the standard, but there are compiler
               | implementations (like Microsoft's) that have added it.
        
               | jcelerier wrote:
               | I just did a gh code search (__finally lang:c++), a
               | hundred-ish instances shows up in ~12 repos mainly
               | related to MS / .Net:
               | 
               |  sleuthkit/scalpel
               | 
               |  microsoft/service-fabric
               | 
               |  dotnet/runtime
               | 
               |  microsoft/Detours
               | 
               |  dotnet/wpf
               | 
               |  Chuyu-Team/VC-LTL
               | 
               |  apache/logging-log4net
               | 
               |  microsoft/Windows-classic-samples
               | 
               |  microsoft/winfile
               | 
               |  dotnet/llilc
               | 
               |  microsoft/DirectXShaderCompiler
               | 
               |  dotnet/diagnostics
               | 
               |  SoftEtherVPN/SoftEtherVPN
               | 
               |  microsoft/PTVS
               | 
               |  aybe/Windows-API-Code-Pack-1.1
               | 
               | I don't think that it's in any way meaningful, it's
               | literally unused given that it's been there for likely
               | 20+ years.
        
             | kaba0 wrote:
             | You mix that up with RAII.
        
             | isomel wrote:
             | Maybe you're mixing up with SEH (Structured Exception
             | Handling) which is a Microsoft extension to C that came
             | even before C++
        
         | usrbinbash wrote:
         | How does "defer" make it harder to reason about the code?
         | Especially when the alternative is a bunch of resource-flag-
         | tests and cleanup labels?
         | 
         | There is a reason why it was introduced in Go.
        
           | yk wrote:
           | Because the compiler has to introduce code implicitly. The
           | control flow is usually very explicit in C, with this feature
           | it is less explicit.
        
       | aaaaaaaaaaab wrote:
       | Just use C++. No need to proliferate the C language.
        
         | neatze wrote:
         | I highly doubt you can explain why.
        
           | kaba0 wrote:
           | Not the parent commenter, but namespaces alone make the
           | switch a no-brainer. Add on top RAII, actual abstractibility
           | (good luck implementing std::string in C properly) make it a
           | strictly worse language.
        
             | daneelsan wrote:
             | Why ado you think namespaces make it a no brainer? I would
             | argue the only thing I would want from C++ it's operator
             | overloading, and I'm not even conviced about that.
        
       | dataangel wrote:
       | It's annoying to see everyone adopting defer (Jai, Go, Zig) and
       | not destructors. Destructors are strictly more powerful. Defer
       | only lets you delay something based on scope, while destructors
       | let you delay based on object lifetime, which might be tied to
       | scope (for an object allocated on the stack) but could also be on
       | the heap. With destructors you can have a smart pointer type
       | where you can't screw up the incrementing and decrementing, with
       | defer you cannot do this.
        
       | jcelerier wrote:
       | Took a few years for C to adopt function prototypes from C++, one
       | can hope that RAII makes it in less than a century
        
         | thechao wrote:
         | I hope it's not mandatory. I'd much prefer static analysis to
         | tell me of use-before-acquire errors: I have a number of hot-
         | inner-loops where initialization nukes performance.
        
           | edflsafoiewq wrote:
           | Can you give an example of how initialization nukes
           | performance?
        
             | thechao wrote:
             | In the context I'm thinking of, I have fairly sizable
             | arrays -- up to 512 bytes; usually, only the first few
             | bytes are used (for state tracking); an `int` tells me the
             | high-water mark so the data is only read after being
             | written. The amount of work int the loop is (in almost all
             | cases) only 5 or 6 instructions before termination.
             | Initializing 512 bytes is _best case_ 8 ops, which is
             | >100% overhead. The code is recursive, but bounded to a
             | depth of 8, wity internal linkage (only two `int`s and a
             | pointer are passed) in the tail call position, so the
             | calling convention is just three registers. Even a compiler
             | like q9x, or clang with no opts, produces excellent code.
             | But, even a _sniff_ of initialization tanks perf to the
             | tune of 3x.
        
               | jcelerier wrote:
               | > Initializing 512 bytes
               | 
               | RAII in C++ does not mean that all bytes are initialized,
               | only the ones you want. In                   struct Foo {
               | int header = 0;           float foo;           int
               | data[64];           int footer;                // just to
               | show that having an explicit constructor / dtor does not
               | change anything           Foo(): footer{456} { }
               | ~Foo() { }         };
               | 
               | only "header" and "footer" will be initialized:
               | https://gcc.godbolt.org/z/4W1WzfPEv
               | 
               | It's not a matter of optimization, absolutely no compiler
               | will zero-initialize unless you explicitely ask for it
               | (or you are in a situation where the language mandates
               | initialization, like global static in C and C++)
        
               | dataangel wrote:
               | > It's not a matter of optimization, absolutely no
               | compiler will zero-initialize unless you explicitely ask
               | for it
               | 
               | Oh man I'm about to ruin your day. Behold the horror:
               | 
               | https://stackoverflow.com/questions/29765961/default-
               | value-a...
        
       | xayfs wrote:
        
       | unwind wrote:
       | In all of those examples that talked about which pointer value to
       | capture, it would perhaps have helped to use the built-in
       | (standard, although in my opinion way too few programmers use it)
       | way of indicating that a value won't change, i.e. const:
       | double * const q = malloc(something);         /* use whatever
       | magic new syntax here, 'q' will not change in this scope */
       | 
       | That wasn't so hard, in my opinion. They could simply let 'defer'
       | fail if the referenced pointer isn't constant. Also, as a micro-
       | wtf I was confused by the use of 'double', for a generic
       | allocation I would have expected void.
        
         | foxfluff wrote:
         | So then you can't for example use defer with a pointer that may
         | get realloc'd in a loop? Or pointer to the root node of tree-
         | to-be-built? What a half-assed defer, in my opinion.
        
       | ohCh6zos wrote:
       | Defer is a bad feature in Go and I'm hesitant to see it spread to
       | other languages. It exists as a hack because error handling in Go
       | is convoluted.
        
         | laumars wrote:
         | It's already exists in other languages and it exists because a
         | function could have multiple different exit points. Even if Go
         | had different error handling there might well be different exit
         | points. Hence why defer isn't a Go-specific feature.
        
         | leni536 wrote:
         | Defer is weird in Go, being tied to function scope instead of
         | immediate block scope.
        
           | ohCh6zos wrote:
           | You're right, that is a better way to put it than what I
           | said.
        
           | assbuttbuttass wrote:
           | Function scope allows constructs like                   for
           | _, lock := range locks {             lock.Lock()
           | defer lock.Unlock()         }
           | 
           | To acquire locks in a loop and release them at the end of a
           | function. Otherwise the previous lock would be dropped each
           | time the loop executes.
           | 
           | I've never seen the advantage to having a block structured
           | defer: it's easy to add a new function, it's not always
           | possible to remove a block.
        
             | jcelerier wrote:
             | > To acquire locks in a loop and release them at the end of
             | a function.
             | 
             | that looks like a super big gotcha, hasn't there been
             | enough bugs with alloca() being called in a loop yet to
             | show how risky and unintuitive this is ? block-scoped is
             | explicit and explicit is good
        
               | kaba0 wrote:
               | Good implementations of defer are scope-based, so those
               | would be clean up at the end of the loop pretty
               | explicitly.
        
             | boardwaalk wrote:
             | How often do you do something like that? Seems pretty rare.
             | If you're managing locks maybe having an actual collection
             | for them makes sense.
             | 
             | If I had to choose between allowing that pattern and
             | allowing reasonable usage in all control constructs, I'd
             | choose the latter.
        
               | Someone wrote:
               | I can see one use that when using lock ordering to
               | prevent deadlocks (http://tutorials.jenkov.com/java-
               | concurrency/deadlock-preven...), but then, the locks to
               | be taken are a fixed set, and one probably would have
               | function _take_locks(lock *)_ and _release_locks(lock *)_
               | , and one could do _defer release_locks_.
               | 
               | Also, Wikipedia (https://en.wikipedia.org/wiki/Deadlock, 
               | https://en.wikipedia.org/wiki/Deadlock_prevention_algorit
               | hms) doesn't seem to know about it. Even though I
               | expect/guess it to be popular in embedded work, that
               | makes me wonder whether lock ordering is used much.
               | 
               | Could also be an omission in Wikipedia. It has
               | https://en.wikipedia.org/wiki/Banker%27s_algorithm, which
               | I hadn't heard of, and that Wikipedia says of
               | 
               |  _"In most systems, this information is unavailable,
               | making it impossible to implement the Banker 's
               | algorithm. Also, it is unrealistic to assume that the
               | number of processes is static since in most systems the
               | number of processes varies dynamically"_
        
             | KerrAvon wrote:
             | I see this code and all I can think is that you're going to
             | be spending the rest of your life debugging threadsafety
             | issues. I can contrive a case where this is useful, but
             | where in the real world?
        
             | pcwalton wrote:
             | The semantics of block-scope defer are much simpler, easier
             | to understand, and faster. The compiler always _statically_
             | knows which defers execute at any given point, which aids
             | optimization. With function-scope defer, the semantics are
             | extremely dynamic, requiring bookkeeping of a runtime stack
             | of defer thunks, and compilers have a hard time optimizing
             | it.
             | 
             | Function-scope defer is something that surprises everyone I
             | explain it to. Programmers naturally expect defer to be
             | block-scoped.
        
             | dzaima wrote:
             | ..Except that in C that'll turn into stack allocations and
             | will easily segfault due to a stack overflow. So you can't
             | actually use it like that in practice (unless you can
             | guarantee your loop will be called a small number of
             | times). You probably even won't notice while writing the
             | code, and will just get random segfaults wherever you have
             | a defer in a loop when you hit a large enough iteration
             | count. Never mind it being very inefficient while it
             | doesn't.
        
               | gmfawcett wrote:
               | Well, we are earnestly analyzing a silly little example
               | program, but okay :). The equivalent C code wouldn't
               | produce stack-allocated mutexes unless that's what the
               | programmer wanted. E.g. the POSIX pthread functions don't
               | care where your mutexes are allocated, since they are
               | always passed by reference.
        
               | dzaima wrote:
               | It's not the mutexes that'd be stack-allocated, but the
               | list of things to call back to at the end of the
               | function. The locks list could be modified or freed by
               | the end of the function, but _something_ still must hold
               | the list of things to deferred-unlock.
               | 
               | The pthread_cleanup_push/pthread_cleanup_pop thing
               | presumably keeps its own heap-allocated vector, backed by
               | malloc or something. C itself can't willy nilly heap-
               | allocate, so that list will be on the stack. But the
               | stack is tiny compared to how long loops can be. Hence
               | stack overflow.
        
               | gmfawcett wrote:
               | C libraries -- including the runtime implementations of
               | features like this -- can heap allocate just fine. They
               | just need to return pointers on the stack to the heap
               | allocated values, either directly or indirectly (e.g.
               | buried in a struct return value). As is always the case
               | with C, the burden to free the memory is on the caller:
               | no problem.
               | 
               | Given a possible implementation, this loopy mutex example
               | could have a tiny stack footprint: a single pointer to
               | the shared cleanup() function; and a single pointer to
               | the head of a (heap allocated) linked list of pointers to
               | mutex (i.e., the function arguments). And the function
               | pointer would not necessarily require allocation at all,
               | as we can statically point at the function definition
               | here. So we are down to a single word of stack
               | allocation.
        
               | pcwalton wrote:
               | Who's going to construct the linked list, and where does
               | it live? That's what the parent comment is pointing out.
               | 
               | In the general case I see no alternative to either the
               | compiler generating one alloca() per defer or heap
               | allocating defer callbacks. Both are terrible solutions
               | for C, because alloca can overflow, while heap
               | allocations can fail with an error code and defer has no
               | way to catch that error. Besides, C programmers just
               | won't use the feature if it requires allocation out of
               | performance concerns. Block-scoped defer is the only
               | reasonable semantics.
        
               | foxfluff wrote:
               | > Besides, C programmers just won't use the feature if it
               | requires allocation out of performance concerns.
               | 
               | Nevermind embedded platforms where heap might be
               | unavailable or just so scarce that its use beyond early
               | initialization is strictly verboten. Or interrupt
               | handlers where you simply can't call an allocator.. Block
               | scoped defer could still be useful on such systems (e.g.
               | with locks).
        
               | gmfawcett wrote:
               | Same question in return: who's going to alloca() or heap-
               | allocate the defer callbacks? How is that substantively
               | different from maintaining a linked list? As soon as
               | compiler support is on the table -- i.e. we're not
               | limited to using some too-clever cpp macrology and a
               | support library -- then virtually any implementation is
               | possible. There's obviously more than one way to do it.
               | 
               | > C programmers just won't use the feature if it requires
               | allocation out of performance concerns.
               | 
               | I agree that _many_ C programmers wouldn 't touch the
               | feature for performance reasons. But let's not pretend
               | that every C program is a video driver, a AAA game or a
               | web engine. Many, many large C programs would benefit
               | immensely from `defer` semantics -- otherwise, why would
               | the GCC feature exist -- and they are performance-
               | tolerant enough that a little heap allocation would be a
               | reasonable tradeoff for increased safety.
               | 
               | But I'm not really defending `defer` in the first
               | place...
               | 
               | > Block-scoped defer is the only reasonable semantics.
               | 
               | I agree with you completely. :) I was never defending
               | function-scoped `defer`, but rather some claims about the
               | _necessity_ of stack allocation that I disagreed with.
               | There are possible defer implementations that wouldn 't
               | blow the stack: that's my only point.
        
               | assbuttbuttass wrote:
               | That's a good point. I was wondering about Zig's defer,
               | which has a block scope, and I suspect it's exactly
               | because of this issue.
               | 
               | Go can allocate defers on the heap, but that's a
               | different story.
        
             | duped wrote:
             | Named scopes would alleviate this problems, and I wish more
             | languages would adopt them.
        
             | gaganyaan wrote:
             | That code is odd (though I assume idiomatic Go). I'd much
             | rather have block scoping with something like this:
             | for _, lock := range locks {             lock.Lock()
             | // Do something with lock         }
             | 
             | And I don't really do Go, but in Rust, if I wanted to lock
             | some arbitrary list of locks for a whole function, it would
             | just be something like this at the top:
             | let _ = locks.iter().map(|l|
             | l.lock().unwrap()).collect::<Vec<_>>();
        
         | bboozzoo wrote:
         | What in your view makes defer a bad feature of Go? Maybe my bar
         | is low, but each time I jump back to C I wish I had defer and
         | end up abusing __attribute__((cleaup)) instead.
        
           | [deleted]
        
           | [deleted]
        
         | SV_BubbleTime wrote:
         | > error handling in Go is convoluted
         | 
         | Is it worse than in C that there is no concept at all? Is it
         | better that everyone is on their own to do it their own way
         | every time?
         | 
         | I'm not above making the eaiser, but to your point when I see
         | their example of:                 double* q =
         | malloc(something);       defer [qp = &q]{ free(*qp); };
         | 
         | That doesn't look like the C that I know.
        
         | halpert wrote:
         | Being able to close an open resource when a function returns is
         | generally useful. It has nothing to do with error handling.
        
         | kgeist wrote:
         | >It exists as a hack because error handling in Go is
         | convoluted.
         | 
         | What's the alternative, though? Sprinkle your code with
         | try..finally's? RAII like in C++?
        
           | w4rh4wk5 wrote:
           | RAII is fine. If it's a language feature, APIs can provide
           | that to you so you don't have to write RAII wrappers all the
           | time.
           | 
           | Otherwise a generic _bracket_ operator could be introduced,
           | similar to Python's `with` statement. In C it might be a bit
           | hard to parameterize, but even if it just requires you to use
           | a void _, it 's still an improvement vs. not having anything
           | at all. (Talking about language features so
           | `__attribute__((__cleanup__))` doesn't count.)
           | 
           | Imagine something like this:                   FILE* handle =
           | bracket(fopen("data.bin", "rb"), fclose) {
           | fread(..., handle);             return;
           | fwrite(..., handle);         } // <-- implicitly calling
           | fclose(handle), always
           | 
           | Edit: After writing this sample I could even an optional
           | `else` block in case `fopen` returned `NULL`.
           | 
           | Edit2: Of course, syntax fine-tuning would be welcome. For
           | instance, `handle` should be restricted to the bracket's
           | scope._
        
           | qsort wrote:
           | Context managers, or an Auto-Closable interface with
           | syntactic support.
           | 
           | Also why is RAII bad? It's an awesome feature in C++.
        
             | kgeist wrote:
             | >Also why is RAII bad? It's an awesome feature in C++
             | 
             | I didn't mean it's bad (I used to be a C++ developer myself
             | and enjoyed RAII a lot), just wondering what are the
             | alternatives that the OP doesn't consider "hacks". RAII
             | would require to introduce constructors/destructors in the
             | language, with all the gotchas (and probably you'll want a
             | full-fledged OOP after that), which is apparently against
             | Go's design principles as a simple language.
             | 
             | >Auto-Closable interface with syntactic support.
             | 
             | I don't see much difference here in practice; the whole
             | difference is that in C#, for example, you use "using" on a
             | whole object, while in Go it's a "defer" on a specific
             | method of the object (or a standalone function). You are
             | not limited to a single method and can use it on any method
             | you deem necessary.
             | 
             | Auto-closeable/RAII, however, is less flexible in ad hoc
             | situations specific to a certain function (you have to
             | define dummy classes just to make sure a function is called
             | no matter what), Go allows to use "defer" on a lambda.
             | Auto-closeable also ties control flow to an object, which
             | makes sense in an OOP-focused language, but Go isn't one.
        
               | woodruffw wrote:
               | I agree with most of your points, but I wanted to also
               | point out that you don't need C++'s ctor/dtor spaghetti
               | to have useful RAII: Rust achieves it without even having
               | first-class constructors (and opt-in custom destructors
               | via Drop).
        
           | the_gipsy wrote:
           | Result types
        
             | kgeist wrote:
             | Defer is used to automatically release resources on
             | function exit, I'm not sure how result types will help
             | here. Can you give an example?
        
               | the_gipsy wrote:
               | It's an alternative to try...finally or RAII.
        
             | [deleted]
        
       | nrclark wrote:
       | C could really use a standardized defer feature.
       | 
       | I don't like the proposed syntax though, because it introduces a
       | new meaning for the & operator. In all other uses, the & operator
       | modifies the type of the operand, to be the opposite of the *
       | operator.
       | 
       | In the proposed syntax, & becomes something a little bit
       | different: a C++-style reference. I think it would introduce too
       | much language confusion.
       | 
       | A more C-like syntax would be:
       | 
       | defer(captures-by-value; captures-by-reference) {}.
       | 
       | Defer() statements would be executed at scope-exit, in last-in
       | first-out order. Standard block scoping would apply for anything
       | declared inside of the defer().
        
         | KerrAvon wrote:
         | The & syntax builds on a separate proposal for C++-style
         | lambdas in C which would introduce that syntax generally.
        
           | SeanLuke wrote:
           | Given C++'s grotesque track record of insane new syntax, this
           | wouldn't seem to be an advantage.
        
       | marcodiego wrote:
       | The new defer proposal has the potential to make code easier to
       | read, remove some uses of goto's and help to fix some resource
       | leaks that are so common on old non-GC languages. Certainly a
       | feature I'm rooting for.
       | 
       | The only other proposal I'm more interested in are the lambda
       | approaches: http://www.open-
       | std.org/jtc1/sc22/wg14/www/docs/n2890.pdf
        
       | eqvinox wrote:
       | > _Whereas gcc's cleanup attribute is attached to functions,
       | POSIX' cleanup functions and the try /finally are attached to
       | possibly nested blocks._
       | 
       | > _This indicates that existing mechanism in compilers may have
       | difficulties with a block model._
       | 
       | Erm. No. GCC's cleanup attribute is attached to nested blocks.
       | Whatever that means for the conclusion there.
       | 
       | How is a pretty basic misunderstanding like this seeping into an
       | ISO WG document?!?
       | 
       | [add.:] in case anyone wants to double check:
       | https://gist.github.com/eqvinox/c062f5a46f3a60b1151fcdf3e91a...
        
         | tialaramex wrote:
         | An individual or handful of people make a proposal, the Working
         | Group looks at the proposal, maybe it gets revised (this is
         | version 2). Working Groups themselves do not, on the whole,
         | produce documents like this.
         | 
         | There is no ISO magic ensuring working groups are all-knowing.
         | There's an excellent chance that no members of WG14 have a deep
         | in-depth knowledge of GCC, or that members who did they weren't
         | particularly interested in that part of this paper (e.g. they
         | were already staunchly for or against, remember ISO Working
         | Groups are democratic, a proposal does not need consensus, so a
         | working group member who thinks your idea is inherently good or
         | bad might just skim the introduction, and move on)
        
       | butterisgood wrote:
       | C is done. It just is. It has had an amazing run and yes we will
       | continue to write code in it - despite all the better
       | alternatives it will probably never die.
        
       | UltraViolence wrote:
       | C should be retired and replaced with compiled C# (if the usage
       | allows for Garbage Collection) or Rust (if deterministic memory
       | management is needed). There's really no use for pulling on a
       | dead horse.
       | 
       | C has had its heyday and should simply curl up and die.
        
         | nmilo wrote:
         | In 50 years everyone will have moved on to the hot new
         | language, but your OS will still run, at least some, C code.
         | Whether you like it or not C has enough inertia to last a
         | really, really long time.
        
         | Shadonototra wrote:
         | this is the worst idea i read today, thanks
         | 
         | people don't understand what "programming" is about anymore
        
           | rsj_hn wrote:
           | I think one difference between a classically trained
           | programmer of a few decades ago and many of the programmers
           | today who entered from javascript or bootcamps or were even
           | self-taught is lack of understanding about all those other
           | systems below you. For example, do you think the OP has heard
           | of Simple Managed C?
           | 
           | C# is great, but it's not a systems language, it depends on
           | piles of C/C++/etc code in order to run.
        
             | pjmlp wrote:
             | Depends on the compiler toolchain.
        
             | timeon wrote:
             | Not sure about bootcamps, but as a self-taught I have
             | respect for C/C++ even if I do not use them. And even if I
             | use Rust/whatever as self-taught I am especially humble
             | because of all the knowledge I'm missing.
        
             | sharikous wrote:
             | To be able to program in C# you don't need any .c file in
             | your whole computer.
             | 
             | Of course you need a lot of binaries which were produced
             | somewhere using low level languages in the process, and you
             | probably need to comply with the C FFI to access a lot of
             | libraries. But nothing that cannot be done with a different
             | low level language.
        
               | rsj_hn wrote:
               | You really on an entire stack that is programmed and
               | maintained in languages like C, and to the degree that
               | these are provided for whatever chipset you are using,
               | yes _you_ can code in C++. And of course you expect these
               | libs to be regularly patched and updated, and released as
               | new platforms become available, etc.
               | 
               | I'm not saying "don't code in higher level languages".
               | I'm saying that not _everyone_ can code in higher level
               | languages. There is a whole stack that needs maintenance
               | and development.
        
       | pornel wrote:
       | Since Microsoft has decided to sabotage C by not implementing
       | anything that isn't in C++ already, and this will never be in
       | C++, this feature is already dead.
       | 
       | Projects that target only GCC and Clang can use
       | __attribute__((cleanup)) without waiting a decade for it.
        
         | xeeeeeeeeeeenu wrote:
         | > Since Microsoft has decided to sabotage C by not implementing
         | anything that isn't in C++ already, and this will never be in
         | C++, this feature is already dead.
         | 
         | This is no longer true:
         | https://devblogs.microsoft.com/cppblog/c11-and-c17-standard-...
        
           | frankohn wrote:
           | Yes it is still true, they just have decided to support newer
           | c standards when they sabotaged c11 by making optional a
           | fundamental feature like complex numbers:
           | 
           | > Support for Complex numbers is currently not planned and
           | their absence is enforced with the proper feature test
           | macros.
           | 
           | Sadly, there is no future for the C language with a
           | corporation like them in the committee. Much better to look
           | at Zig or Nim, they fill more or less the same space and are
           | developed by smart and passionate people.
        
             | pjmlp wrote:
             | > A compiler that defines __STDC_IEC_559_COMPLEX__ is
             | recommended, but not required to support imaginary numbers.
             | POSIX recommends checking if the macro _Imaginary_I is
             | defined to identify imaginary number support.
             | 
             | They have not sabotaged anything when the feature is
             | optional to start with.
             | 
             | If ISO wanted everyone to actually support it, it wouldn't
             | be optional.
        
       | nrabulinski wrote:
       | "Simple" defer feature - proceeds to implement c++ closures and
       | semantics just to have a defer keyword which invokes them at the
       | exit point of a scope
        
       | rwmj wrote:
       | Ugh please don't do this. Just standardize the existing
       | __attribute__((cleanup)) mechanism which is already implemented
       | by compilers and widely used in many free software projects.
        
         | kps wrote:
         | "this feature cannot be easily lifted into C23 as a standard
         | attribute, because the cleanup feature clearly changes the
         | semantics of a program and can thus not be ignored."
        
           | nmilo wrote:
           | Then it doesn't need to use the existing attribute syntax,
           | which requires that attributes can be ignored. They can make
           | a new syntax.
        
           | rwmj wrote:
           | That's a very weak objection. In any case since most programs
           | are hiding __attribute__((cleanup)) in a macro (eg. glib's
           | g_autoptr macro or systemd's family of _cleanup_* macros) you
           | could use another kind of annotation.
           | 
           | The point here is that the C working group should be
           | standardizing existing practice and helping the existing
           | users of C, and not striking out making bizarre new syntax
           | choices and keywords which are completely unproven.
        
         | 10000truths wrote:
         | __attribute__((cleanup)) is ugly because for some reason, the
         | gcc folks decided that the parameter passed to cleanup needed
         | to be a function pointer of signature void(void**) instead of
         | the much more sane void(void*), with no way to allow implicit
         | casting of the parameter to a function pointer of different
         | type, so none of the basic cleanup functions (e.g. free,
         | fclose) work with it out of the box - you need to pollute your
         | codebase with one wrapper for each cleanup function that you
         | want to use.
         | 
         | They should have made it so that cleanup took an expression
         | block:                   void* foo __attribute__((cleanup({
         | free(foo); }))) = malloc(bar);
         | 
         | Or at least allow a statement expression returning a compile-
         | time known function pointer:                   void* foo
         | __attribute__((cleanup(({ void wrapper(void** p) { free(*p); };
         | wrapper })))) = malloc(bar);
         | 
         | Sure, it still looks ugly, but at least a single generic macro
         | would be able to clean it up and make it look better.
        
           | eqvinox wrote:
           | The "good" way to go about that would be to have a second
           | "cleanup_value" attribute.
           | 
           | (Or, since the standard would be creating new names, have
           | "cleanup" and "cleanup_ptr" instead. Assuming that this is a
           | separate attribute namespace, which I believe it is[?])
           | 
           | FWIW, you do sometimes need the address of the variable.
           | Particularly if it's some struct that is made a member of
           | some container (e.g. linked list) temporarily - you need the
           | original variable's address to unlink it.
        
           | kaba0 wrote:
           | Oh my God, why do we put up with C at all?
        
             | gmfawcett wrote:
             | What are you going to do about it? I suppose you could
             | delete all the C software from your system. Or make a list
             | of all the C programs that your system depends on, and
             | persuade each of their maintainers to change languages.
             | 
             | Or you could just put up with C, like everybody else does,
             | and be quietly thankful that the myriad layers of our
             | modern, complex systems are being maintained by other
             | people.
        
           | rwmj wrote:
           | Sure - I agree! But, it exists and it's widely used already.
           | The C working group should concentrate on standardizing
           | existing practice and not start striking out on unproven and
           | weird new syntaxes and keywords.
        
         | [deleted]
        
       | emptybits wrote:
       | I'm interested. So _defer_ is one of the handful of achievable
       | goals I find Zig interesting and hold some hope for it. Easy to
       | call C from Zig or Zig from C. And Zig can also compile C and
       | target cross-platform easily.                   const
       | sprite_sheet_surface = c.SDL_LoadBMP("res/tile.bmp") orelse {
       | c.SDL_Log("Unable to create BMP surface from file: %s",
       | c.SDL_GetError());             return
       | error.SDLInitializationFailed;         };         defer
       | c.SDL_FreeSurface(sprite_sheet_surface);
        
       | Animats wrote:
       | That doesn't belong in C. C doesn't have much in the way of
       | implicit execution. C++ does, and C++ already has destructors.
        
       | Shadonototra wrote:
       | i prefer D's approach                   scope (exit), scope
       | (failure), scope (success)
       | 
       | https://tour.dlang.org/tour/en/gems/scope-guards
        
       | AlbertoGP wrote:
       | I list a few defer-style approaches for C that I know of,
       | including the previous version of this one, in the "Related work"
       | section of my block-based defer for C (presented in HN last
       | August https://news.ycombinator.com/item?id=28166125):
       | 
       | https://sentido-labs.com/en/library/cedro/202106171400/#defe...
        
       | abaines wrote:
       | I feel as though they should change the name of the paper,
       | because this must be one of the most complex takes on 'defer'
       | that I have seen.
       | 
       | They make a classic mistake of trying to solve a problem by
       | adding more complexity. They cannot decide on by-value or by-
       | reference, so they take the convoluted C++ syntax to allow
       | specifying one (as well as other semantics?).
       | 
       | It does not fit with C in my opinion. It will also hurt third
       | party tools that wish to understand C source code, because they
       | must inherit this complexity.
       | 
       | I would prefer a solution where they simply pick one (by ref / by
       | value) and issue a compiler warning if the programmer has
       | misunderstood. For example, pick by-value and issue a warning if
       | a variable used inside the defer is reassigned later in the
       | scope.
        
         | tedunangst wrote:
         | I think people are getting hung up on the lambda syntax, but it
         | seems they're just taking what they're given. If c23 adds
         | lambdas, at this point I'd say it's more likely to be c++
         | syntax than c syntax because c++ is already out there. So
         | instead of "to resolve ambiguity we force user to choose" it's
         | more like "the probable lambda syntax already makes this
         | explicit so we will use it too." I think that makes more sense
         | than two different syntaxes for lambda and defer, whatever the
         | other merits of the proposal.
        
       | colonwqbang wrote:
       | Why are lambdas needed for this? The standard C idiom used for
       | this since forever ("goto out") does not use lambdas.
        
         | daneelsan wrote:
         | Huh that's a good point.
        
       | Ericson2314 wrote:
       | I dunno this capture stuff is alright but also a bit like blatant
       | appeasement.
       | 
       | The point of this feature is to be non-cannonical RAII. I.e. it
       | is about cleaning up a _location_. This data oriented approach is
       | similar to how  "locked data" is more correct and intuitive than
       | "critical sections".
       | 
       | Now I get C being low level, so perhaps this is better, but I
       | can't really imagine the thing I am cleaning up (as opposed to
       | auxiliary info like an allocator to deallocate memory with) being
       | captured by value.
       | 
       | BTW, speaking of moves, a "set this bool if I use this variable
       | by value" feature would be _very_ useful. Skip C++ 's mistake and
       | do Rust's model. Would work great with this feature.
        
         | lelanthran wrote:
         | > Now I get C being low level, so perhaps this is better, but I
         | can't really imagine the thing I am cleaning up (as opposed to
         | auxiliary info like an allocator to deallocate memory with)
         | being captured by value.
         | 
         | I was wondering the same thing; what is the use-case for
         | needing a capture-by-value option at scope exit?
         | 
         | Just make all captures a capture-by-reference and it works for
         | all use-cases.
        
       | greenn wrote:
       | If the purpose of defer is to replace the `goto single_exit`, I
       | think the value captures are unnecessary. Any single_exit I've
       | implemented uses the value at the time of function exit, so that
       | I can realloc in the middle of the function.
       | 
       | It would be a shame if this defer was limited to function scope.
       | It would be very useful in nested blocks as well. But, I would
       | still appreciate it.
       | 
       | defer and auto are the only things I would love to see in C.
        
       | daneelsan wrote:
       | Some say they don't like it because it it's implicit control
       | flow, i.e. I don't like that my code is being put into the end of
       | the function without it being in the end of the function. I mean
       | OK, but that's what for loops so right? The i++ is put at the end
       | of the "while" loop, together with the exit condition. I think
       | the more important problems are: 1) why be function scoped and
       | not block scoped? 2)why should it be so tied up with lambdas?
       | 
       | 1) I don't se why they chose function over scoped so please
       | enlighten me 2) the proposal said there were debates whether
       | defer free(ptr) refers to the ptr where the defer first appears,
       | or to the current ptr that appears when the defer block is being
       | executed. As someone mentioned, gotos already work in the latter
       | way. Same goes with i++ in a for loop, I could do whatever I
       | wanted with the i inside the for, and the i++ or the exit
       | condition would use the latest value of i, not the value at the
       | start of the block.
        
       | AndyKelley wrote:
       | What an arrogant use of the word "simple" in the title.
       | 
       | * no decision on access to variables
       | 
       | * lambdas are involved at all
       | 
       | * "appearance in blocks other than function bodies [is]
       | implementation-defined"
       | 
       | They cited D in the implementation, but then used Go as the
       | inspiration for the feature. The answer was staring them right
       | there in the face, scope(exit) [1]. Or, you know, they could have
       | cited Zig for the exact syntax they wanted [2].
       | 
       | This feature is completely unusable.
       | 
       | Let me demonstrate. You can't use this inside an if statement:
       | if (foo) {             const ptr = malloc(...);             defer
       | [&]{ free(ptr); } // implementation defined. get fucked         }
       | 
       | Furthermore, it's go-style, so the defer runs at the end of the
       | function. This is not only implementation defined, it's full-
       | blown undefined behavior because `ptr` goes out of scope before
       | the defer expression runs.
       | 
       | Scope-based defer works great. Go-based defer is already
       | problematic enough in Go; in C it's worse than not having defer
       | in the language at all.
       | 
       | [1]: https://dlang.org/spec/statement.html#scope-guard-statement
       | 
       | [2]: https://ziglang.org/documentation/0.9.0/#defer
        
         | [deleted]
        
         | dnautics wrote:
         | Oh come on now, it's not _completely_ unusable, just  "unusable
         | in a ton of important use cases".
        
           | AndyKelley wrote:
           | Point taken, but I will defend my claim: The few use cases
           | where it does work are footguns because in the future, you or
           | another collaborator will be tempted to wrap the code into a
           | block, which is normally 100% safe for all other features of
           | the language. It would be easy to do without thinking about
           | it. But if you do it becomes UB as demonstrated above.
           | 
           | So the reasonable policy would be to use the same cleanup
           | method everywhere, to avoid footgun firing when code is
           | edited.
        
         | pjmlp wrote:
         | Yep, typical C decision design pattern.
        
       | nmilo wrote:
       | I really don't like it. The lambda stuff, especially the captures
       | that were ripped from C++, don't fit C at all. The question about
       | whether it should be cleaned up at end-of-scope or end-of-
       | function is too ambiguous and debated. And if I'm understanding
       | this correctly, this is the worst part:
       | 
       | > This indicates that existing mechanism in compilers may have
       | difficulties with a block model. So we only require it to be
       | implemented for function bodies and make it implementation-
       | defined if it is also offered for internal blocks.
       | 
       | How will that work?
        
         | massinstall wrote:
         | I agree and don't like it either. +1
        
         | bangonkeyboard wrote:
         | I was, and continue to be, very disappointed that the (to me)
         | more natural and C-like block syntax for lambdas was rejected
         | from C11 (http://www.open-
         | std.org/jtc1/sc22/wg14/www/docs/n2030.pdf).
        
           | b3morales wrote:
           | In practice those signatures can get a bit tiresome to write
           | out, but I agree, they fit much, _much_ better with C syntax
           | and mindset.
        
             | bangonkeyboard wrote:
             | Certainly, but no more tiresome than the existing C syntax
             | for function pointers, to which they were direct analogues.
        
               | b3morales wrote:
               | Yes, agreed absolutely. I think they'd be used more
               | heavily because of the convenience of allowing captures,
               | though.
        
           | nmilo wrote:
           | I think that idea is pretty cool. I don't like the new
           | closure pointer type, and how all functions that take
           | function pointers need to be retrofitted to take closures as
           | well, but I guess it's necessary. I assume closure pointers
           | would be implemented as a double pointer, one void* to point
           | to the captured variables in memory, and one function pointer
           | to point to the procedure. One of the most annoying parts of
           | C is how callback logic needs to be defined completely
           | separately from the calling logic.
        
         | jimjams wrote:
         | Since variables can be scoped to blocks, and allocation scopes
         | can be arbitrary, needing neither a function or block scope to
         | bracket them, I don't see how that will fly either.
        
       | karmakaze wrote:
       | The Java AutoCloseable seems like a cleaner version of the MS
       | __try/__finally:                 try (MyCloseable c =
       | useSomething()) {         // stuff       }
       | 
       | Why isn't this the path explored? The main difference would be
       | not having extra nested blocks. That seems relatively minor, no?
       | It can be improved arg list:                 try (MyCloseable c1
       | = useSomething(), ElseThing c2 = elseThing()) {         // stuff
       | }
        
         | CodesInChaos wrote:
         | This syntax relies on destructors, which C doesn't have.
        
           | kaba0 wrote:
           | One could pass in a cleanup function that will be called -
           | but this is very similar to gcc's cleanup attribute.
        
       | cyco130 wrote:
       | Isn't this scope guard:
       | https://tour.dlang.org/tour/en/gems/scope-guards ?
        
       | david2ndaccount wrote:
       | I usually use the single-exit-point idiom with gotos to handle
       | cleanup. I now wonder if there is a compiler that helps you
       | enforce that so you don't randomly stick a return in the middle
       | of your function.
       | 
       | Anyway, this would be great to have in C as it would simplify how
       | resources are handled.
        
         | 95014_refugee wrote:
         | You can use the cleanup attribute and a flag to detect early
         | exit.
        
         | loeg wrote:
         | You can use the non-standard, but typically implemented,
         | cleanup attribute today for function-scoped cleanup. I don't
         | know if compilers inline this kind of "destructor". It works
         | well -- I've seen it used successfully at multiple employers.
        
           | rwmj wrote:
           | Not only inline it, but also optimize it away (eg.
           | _cleanup_free char *ptr = NULL; the cleanup path just
           | disappears in places before ptr is assigned to a non-NULL
           | value).
        
             | loeg wrote:
             | Great!
        
       | lerno wrote:
       | I don't mind a sane defer in C, that is something which follows
       | scope in the same way stack allocated destructors are invoked.
       | This follows the behaviour in other languages like Swift.
       | 
       | Why "on function exit" style defers - already known to be a bad
       | idea from Go - is beyond me. Such a solution more or less
       | requires storage of potentially unbounded allocations.
       | Foo *f;         for (int i = 0; i < very_big_value; i++) {
       | f = get_foo(i);           // BOOM           defer return_foo(f);
       | }
       | 
       | I would sort of understand if this was proposed for a high level
       | language garbage collection where actual memory usage is
       | secondary.
        
         | cpuguy83 wrote:
         | In this case you wrap the loop in an anonymous function if you
         | need it to be cleaned up within the scope of the loop. Or move
         | the functionality to another function.
        
           | knome wrote:
           | Adding a statement that would require a dynamic allocation
           | for every iteration of a loop is kind of insane for C.
           | 
           | It doesn't matter what the defer does, if it's allocated in a
           | for statement and doesn't go off until the surrounding
           | function exits, then it's going to have to stuff tons of
           | function pointers and parameters somewhere, presumably
           | alloca'd onto the stack over and over.
           | 
           | That's not C. That shouldn't be C.
           | 
           | There are a dozen languages for doing clever dynamic magical
           | things in code. C is still fairly straightforward. Use one of
           | the other languages instead of bagging down C with complexity
           | until it turns into another difficult C++ variant over time.
        
         | skybrian wrote:
         | It seems like you could just ban that. Defers aren't typically
         | used within loops, so why support it?
        
           | clysmic wrote:
           | Sure they are, if there is something that is being acquired
           | at the start of the loop and needs to get released at the
           | end...
        
             | skybrian wrote:
             | ...of the function? You could append to a slice, after
             | setting up a defer to free everything in the slice.
             | 
             | ...of the loop? Call a function.
        
         | adamrt wrote:
         | > Why "on function exit" style defers - already known to be a
         | bad idea from Go - is beyond me
         | 
         | Is there something you can point me to about this? I write Go
         | professionally and from a readability and utility standpoint I
         | really like it in common scenarios. I hadn't heard its a know
         | bad idea and am just curious. Thanks.
        
           | kaba0 wrote:
           | I think parent means that there are languages with scope-
           | based clean-up (e.g. in rust/c++ a value will be cleaned up
           | at the end of the _scope_ that contained it, so one can even
           | create a separate block inside a function) which is a better
           | choice than forcing people to do clean up at the end of the
           | function.
        
       | dandotway wrote:
       | Features that seem like a good idea at the time often don't stand
       | the test of time 20-30 years in the future. In the mid-90s
       | Object-Oriented Programming was super-hyped so a bunch of other
       | languages bolted on OO, such as Fortran and Ada. But now we have
       | Go/Rust/Zig rejecting brittle OO taxonomies because you always
       | end up having a DuckBilledPlatypus that "is a" Mammal and "is a"
       | EggLayer.
       | 
       | A great strength of C is that if you want more features you just
       | go to a subset of C++, no need to add them to C. C++ is the big,
       | ambitious, kitchen-sink language. When C++ exists we don't need
       | to bloat C.
       | 
       | Fortran was originally carefully designed so that people who
       | aren't compiler experts can generate very fast (and easily
       | parallelized) code working with arrays the intuitive and obvious
       | way. But later Fortran added OO and pointers making it much
       | harder to auto-parallelize and avoid aliasing slowdown. Now that
       | GPUs are rising it turns out that the original Fortran model of
       | everything-is-array-or-scalar works really well for automatically
       | offloading to the GPU. GPUs don't like method-lookup tables, nor
       | do they like lambdas which are equivalent to stateful Objects
       | with a single Apply method.
       | 
       | Scientists are moving to CUDA now, which on the GPU side deletes
       | all these features that Fortran was bloated with. Now nVidia
       | offers proprietary CUDA Fortran which is much more in the spirit
       | of original Fortran, deleting OO and pointers for code that runs
       | on GPU. If the ISO standards committee didn't ruin ISO Fortran
       | for scientific computing by bloating it with trendy features we
       | could all be running ISO Fortran automatically on CPUs and GPUs
       | with identical code (or just a few pragmas) and not be locked in
       | to proprietary nVidia CUDA.
       | 
       | But GPUs are now mainly used for crypto greed instead of science
       | for finding cancer cures or making more aerodynamic aircraft so
       | maybe it all doesn't matter anyway.
        
         | svnpenn wrote:
         | > A great strength of C is that if you want more features you
         | just go to a subset of C++, no need to add them to C. C++ is
         | the big, ambitious, kitchen-sink language. When C++ exists we
         | don't need to bloat C.
         | 
         | This is a rationalization, and a bad one. When your solution is
         | "just pull in another programming language", you have a
         | problem.
        
         | mst wrote:
         | > GPUs don't like method-lookup tables, nor do they like
         | lambdas which are equivalent to stateful Objects with a single
         | Apply method.
         | 
         | Since I tend towards read-only instance data, I often live my
         | life considering an object to mostly be a bag of closures with
         | a shared outer scope.
        
         | bee_rider wrote:
         | Yeah. I think I'm much less informed on this topic, but my
         | initial thought on reading the "Rationale" section was that
         | this sort of feature would only be helpful in cases where C
         | offered almost no advantages over C++.
        
       | fshee wrote:
       | Somewhat related: I hacked some macro up in gnu89 C to get a Go
       | styled defer once upon a time. I felt bad for making my compiler
       | course instructor review that code... Very convenient, though.
        
       ___________________________________________________________________
       (page generated 2022-01-16 23:00 UTC)