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