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