Defer: Resource Cleanup in C with Gccs Magic
Posted3 months agoActive3 months ago
oshub.orgTechstory
calmmixed
Debate
70/100
C ProgrammingResource ManagementCompiler Features
Key topics
C Programming
Resource Management
Compiler Features
The article explores implementing a 'defer' statement in C using GCC's cleanup and nested functions, sparking discussion on its usefulness and potential pitfalls.
Snapshot generated from the HN discussion
Discussion Activity
Very active discussionFirst comment
N/A
Peak period
41
24-30h
Avg / period
9.8
Comment distribution88 data points
Loading chart...
Based on 88 loaded comments
Key moments
- 01Story posted
Sep 30, 2025 at 3:03 AM EDT
3 months ago
Step 01 - 02First comment
Sep 30, 2025 at 3:03 AM EDT
0s after posting
Step 02 - 03Peak activity
41 comments in 24-30h
Hottest window of the conversation
Step 03 - 04Latest activity
Oct 3, 2025 at 11:16 AM EDT
3 months ago
Step 04
Generating AI Summary...
Analyzing up to 500 comments to identify key contributors and discussion patterns
ID: 45422717Type: storyLast synced: 11/20/2025, 3:35:02 PM
Want the full context?
Jump to the original sources
Read the primary article or dive into the live Hacker News thread when you're ready.
[0] https://thephd.dev/c2y-the-defer-technical-specification-its...
[1] https://patchwork.ozlabs.org/project/gcc/list/?series=470822
[2] https://github.com/ludocode/onramp
[3] https://github.com/fuhsnn/slimcc
> If malloc fails and returns NULL, the cleanup function will still be called, and there’s no simple way to add a guard inside free_ptr.
free(NULL) is a no-op, this is a non-issue. I don't know what's so hard about a single if statement anyway even if this were an issue.
>If ptr is a null pointer, no action occurs.
> This option is enabled by default on most targets. On AVR and MSP430, this option is completely disabled.
>so that if a pointer is checked after it has already been dereferenced, it cannot be null.
sound to me that if i've never deref the pointer anytime before(e.g the null check is at the beginning of function), the compiler won't remove this check.
If you check after dereferencing it, yes it can. But in this case why would you not check before dereferencing? It's the only UB-free choice.
To be more precise GCC says "eliminate useless checks for null pointers" and what I am saying that you can never be sure what in your code ended up being "useless check" vs "useful check" according to the GCC dataflow analysis.
Linux kernel is a famous example for disabling this code transformation because it is considered harmful. And there's nothing harmful with the nullptr check from your example.
Right. It's UB. And that's why the optimization in question is about removing that check. The only reason the optimization is valid for a C compiler to do, is that it can assume dereferencing a null pointer lands you in UB land.
I'm sorry, either you are terrible at trying to explain things, or you have thoroughly misunderstood what all this is about. GCC cannot, under any circumstances or with any flags, remove an "if (ptr == NULL)" that happens before dereferencing the pointer.
What this flag is about, and what the kernel bug you mentioned (at least I think you're referring to this one) is about, was a bug that went "int foo = ptr->some_field; […] if (ptr == NULL) { return -EINVAL; }". And GCC removed the post-deref null pointer check, thus making the bug exploitable.
From the help text:
> if a pointer is checked after it has already been dereferenced, it cannot be null.
after. Only applies after. A check before dereferencing can never be removed by the compiler.
Obviously.
I don't think so. If it could, then this code would reliably crash:
That never crashes.Embedded systems are an exception, though. They may not have a MMU, and in such a case the operation will succeed.
This code will NEVER deference a null pointer. Not under any compiler, not with any compiler options:
> A null pointer is not a valid pointer in a predominant number of systems in existence.No, that's not quite pedantically accurate. A null pointer is not a valid pointer in the C programming language. Address zero may or may not be, that's outside the scope of the C language. Which is why embedded and kernel work sometimes has to be very careful here.
> They may not have a MMU, and in such a case the operation will succeed.
Lack of MMU does not mean address zero is valid. It definitely* doesn't make a null pointer valid. In fact, a null pointer may not point to address zero.
The prevailing number of modern systems do not map the very first virtual (the emphasis is on virtual) memory page (the one that starts from zero) into the process address space for pragmatic reasons – an attempt to dereference a zero pointer is most assuredly a defect in the application. Therefore, an attempt to dereference a zero pointer always results in a page fault due to the zeroeth memory page not being present in the process' address space, which is always a SIGSEGV in a UNIX.
Embdedded systems that do not have a MMU will allow *ptr where «ptr» is zero to proceed happily. Some (not all) systems may even have a system specific or a device register mapped at the address being 0.
You are conflating several unrelated things, and there is no pedantry involved – it is a very simple matter with nothing else to debate.
Well… sometimes. If you set a pointer to literal 0, you do not actually make that pointer point to address zero, from the C language's point of view. No, you are then setting it to be the null pointer. (c99 6.3.2.3 paragraph 3)
Now, what is the bit value of a null pointer? That's undefined.
So how do you even set a pointer to point to address zero? In the C standard, maybe if you set an intptr_t to 0 and then cast it to the pointer? Actually I don't know how null pointer interacts with intptr_t 0. Is intptr_t even guaranteed to contain the same bit pattern? I don't see it. All I see is that it's guaranteed to convert back and forth without loss. For all I can find in the spec, converting between intptr_t and pointer inverts the bits.
A null pointer "is guaranteed to compare unequal to a pointer to any object or function".
Did you put an object or function at address zero? Sounds pretty UB to me.
> modern systems […] SEGV
I already agreed with you on this. I mean… now modern systems don't let applications map address zero (actually, is that always true? I know OpenBSD stopped allowing it after some security holes. I'm too lazy to check if Linux did too)
More info at https://stackoverflow.com/questions/63790813/allocating-addr...
In any case, this is a fix that's only like 10 years old (or I'm old and it's actually 20). It used to be possible.
> Embdedded systems that do not have a MMU will allow *ptr where «ptr» is zero to proceed happily.
This is absolutely not true. An embedded system could have I/O mapped to address zero reboot the machine on read or write. And that'd be perfectly fine for the C language spec, since C doesn't allow dereferencing a null pointer.
MMU is not the only way memory becomes magic. In fact, it's probably the LEAST of the magic memory mapping that can happen.
> with nothing else to debate.
I mean… you're just wrong. I'm not conflating unrelated things. I'm correcting multiple unrelated mis-statements you made.
To add the things up though: Let's say you intend to read from address zero, so you do `char* ptr = 0; something(*ptr);`. C standard would allow this to set ptr to 0xffff, and reading from that address starts the motor. The C standard doesn't say. It just says that assigning 0 sets it to null pointer, which on some systems is 0xffff.
I've certainly worked on embedded stuff that "did stuff" when an address was read. Sometimes because nobody hooked up the R/W pin, because why would they if the address goes to a motor where "read" doesn't mean anything anyway?
So if you run that code on a system before the MMU is activated or on a system without a MMU, «main» will return 0 on all systems[0] (if the memory is initialised with zeroes). You do have a point that some embedded systems[1] may have device registers mapped at 0, but that bears no relevance on the generated code – it will still attempt to read the 0th address.
You can also test the generated code in QEMU on an architecture of your choice in the «bare metal mode» (i.e. memory protection off) and observe that a read from 0 will give you 0 if the first memory page is filled with 0s.
> More info at https://stackoverflow.com/questions/63790813/allocating-addr...
You are most assuredly conflating a pointer to 0 dereferencing with the memory protection/virtual memory management system, and the explanation is in the first answer. It is Linux that implements a kernel-level check in mmap(2) on the address to mmap into, not the hardware. It is a Linux-specific quirk, and other UNIXes will allow the mmap to 0 to proceed but reading from 0 will still yield a SIGSEGV due to memory protection being in use.
> MMU is not the only way memory becomes magic. In fact, it's probably the LEAST of the magic memory mapping that can happen.
MMU is not magic. It is a simple and very efficient design that works in concert with the microarchitecture it has been implemented for – CPU traps, memory page descriptors and tables.
> I mean… you're just wrong. I'm not conflating unrelated things. I'm correcting multiple unrelated mis-statements you made.
Respectfully, so far I am yet to see a single compelling argument or tangible piece of evidence to support the claims you have espoused. I have provided a few very concrete and specific examples as supporting evidence, but I am not seeing the same on your side.
[0] The only exception that does not initialiase memory with zeroes that I am aware of is AIX (but not POWER/PowerPC that it runs on!) – the AIX VMM initialises a new memory page upon allocation with 0xdeadbeef to make unintialised pointers forcefully crash the process. Linux, *BSD's running on POWER/PowerPC do not do it, it is an AIX specific quirk.
[1] Again, embedded may have a nuance (subject to a specific hardware* implementation) as it is a commonplace in embedded systems to not* have a contiguous memory space and have holes in it, including the zeroeth address. It does not preclude the generated code to attempt to access 0, though, if the hardware supports it.
No, certainly not, but you can do
`if(ptr == NULL) return;`
which is correct but unnecessary since `free` is required to do that check.
I'm pretty certain that `free(NULL)` is part of the C99 standard, so compiler vendors have had 25 years to address it.
If your `free(NULL)` is crashing on a certain platform, you probably have bigger problems, starting with "Compiler that hasn't been updated in 25 years".
No, of course it won't. `free(NULL)` has been a noop ever since C89 (and before, for that matter).
> The free function causes the space pointed to by ptr to be deallocated, that is, made available for further allocation. If ptr is a null pointer, no action occurs. Otherwise, if the argument does not match a pointer earlier returned by a memory management function, or if the space has been deallocated by a call to free or realloc, the behavior is undefined.
Emphasis mine
If I use RAII I'd need to have a struct/class and a destructor.
If I use defer I'd just need the keyword defer and the free() code. It's a lot more lean, efficient, understandable to write out.
And with regards to code-execution timing, defer frees me from such a burden compared to if-free.
Yeah, and not accidentally forgetting to call it. That's the big part. And before "True Scotsman will always free/close/defer!" - No, no they won't.
Unless the compiler screams at them, or its enforced via syntax constructs, it will always slip through the cracks.
Also get over it. We got post-processor things like static analyzers, etc, and whatever AI code reminders/fixers that are coming up next. I'd prefer those over muddying up the code base.
Sure. But unless it's part of compiler, someone will not run it, or will run out of resources (no net or no tokens).
Defaults matter a ton.
RAII doesn't make sense without initialization.
Are you proposing C should add constructors, or that C should make do without defer because it can't add constructors?
Rust has RAII and does not have constructors.
How do you mandate initialization, handle copies, move objects, prevent double frees? What's RAII without any of that?
You pick C because you want a language that doesn't require a variable to be initialised before mutably referencing it, and you write your defer statements or "destructors" defensively, expecting that a variable could be in any state when it comes time to dispose of it.
Or if you find that unacceptable, you accept that C isn't the language you want. There's many other choices available.
Whereas it's perfectly possible to only defer a statement when you know the "object" has been properly initialized.
That's why defer makes sense in a language like C (even Go), but RAII does not.
Cause, like, that's the entire thread.
I'm currently working with Arduino code and the API is a mess. Everything has a second set of manual constructor/destructor, which bypasses type-safety entirely. All only to shoehorn having existing, but uninitialized objects into C++.
(C++ lets you malloc and then placement new (just casting the pointer like C does is UB, but it's being fixed for trivial types) and Rust has both plain alloc and Box<MaybeUninit<T>>)
There are a lot of other reasons not to use them, but yours is a made up strawman.
Yes, that's heap allocation. I'm talking about automatic allocation, by the compiler not getting a pointer from a library function. Like that:
This will call the constructor, which forces me to write the class in a way that has `bool initialized`, and provide a random other method with poses as a second constructor. And now every function has to do a check, whether the constructor was called on the object or I just declare it to be UB and completely loose type safety.With RAII you need to leave everything in an initialized state unless you are being very very careful - which is why MaybeUninit is always surrounded by unsafe
f must be initialized here, it cannot be left uninitialized EVERY element in my_vector must be initialized here, they cannot be left uninitialized, there is no workaroundEven if I just want a std::vector<uint8_t> to use as a buffer, I can't - I need to manually malloc with `(uint8_t)malloc(sizeof(uint8_t)*10000)` and fill that
So what if the API I'm providing needs a std::vector? well, I guess i'm eating the cost of initializing 10000 objects, pull them into cache + thrash them out just to do it all again when I memcpy into it
This is just one example of many
another one:
with raii you need copy construction, operator=, move construction, move operator=. If you have a generic T, then using `=` on T might allocate a huge amount of memory, free a huge amount of memory, or none of the above. in c++ it could execute arbitrary code
If you haven't actually used a language without RAII for an extended period of time then you just shouldn't bother commenting. RAII very clearly has its downsides, you should be able to at least reason about the tradeoffs without assuming your terrible strawman argument represents the other side of the coin accurately
Which hardly ever makes sense, and is possible with clean C++ anyway...
That's news to me; how?
Usually Arduino code is written by hobbyist that give zero care about "clean and abstraction".
https://github.com/embassy-rs/static-cell
Because it’s nowhere near “almost decent RAII” and RAII requires a lot more machinery which makes retrofitting RAII complicated, especially in a langage like C which is both pretty conservative and not strong on types:
- RAII is attached to types, so it’s not useful until you start massively overhauling code bases e.g. to RAII FDs or pointers in C you need to wrap each of them in bespoke types attaching ownership
- without rust-style destructive moves (which has massive langage implications) every RAII value has to handle being dropped multiple times, which likely means you need C++-style copy/move hooks
- RAII potentially injects code in any scope exit, which I can’t see old C heads liking much, if you add copy/move then every function call also gets involved
- Because RAII “spreads” through wrapper types, that requires surfacing somehow to external callers
Defer is a lot less safe and “clean” than RAII, but it’s also significantly less impactful at a language level. And while I very much prefer RAII to defer for clean-slate design, I’ve absolutely come around to the idea that it’s not just undesirable but infeasible to retrofit into C (without creating an entirely new language à la C++, you might not need C++ itself but you would need a lot of changes to C’s semantics and culture both for RAII to be feasible).
https://thephd.dev/just-put-raii-in-c-bro-please-bro-just-on... has even more, mostly from the POV of backporting C++ so some items have Rust counterpoints… with the issue that they tend to require semantics changes matching Rust which is also infeasible.
So, I completely understand the sentiment, but feel that `defer` is a feature that should hopefully move in the opposite direction, allowing us to rely on less exotic code and expose & resolve some of the surprising failure paths instead!
However they rely on Trampolines: https://gcc.gnu.org/onlinedocs/gccint/Trampolines.html
And trampolines need executable stack:
> The use of trampolines requires an executable stack, which is a security risk. To avoid this problem, GCC also supports another strategy: using descriptors for nested functions. Under this model, taking the address of a nested function results in a pointer to a non-executable function descriptor object. Initializing the static chain from the descriptor is handled at indirect call sites.
So, if I understand it right, instead trampoline on executable stack, the pointer to function and data is pushed into the "descriptor", and then there is an indirect call to this. I guess better than exec stack, but still...
(and I hope we get a solution without trampolines for the remaining cases as well)
The always_inline keyword takes care of that here.
If we're referring to the "C is a subset of C++" / "C++ is a superset of C" idea, then this just hasn't been the case for some time now, and the two continue to diverge. It came up recently, so I'll link to a previous comment on it (https://news.ycombinator.com/item?id=45268696). I did reply to that with a few of the other current/future ways C is proposing/going to diverge even further from C++, since it's increasingly relevant to the discussion about what C2y (and beyond) will do, and how C code and C++ code will become ever more incompatible - at least at the syntactic level, presuming the C ABI contains to preserve its stability and the working groups remain cordial, as they have done, then the future is more "C & C++" rather than "C / C++", with the two still walking side-by-side... but clearly taking different steps.
If we're just talking about features C++ has that C doesn't, well, sure. RAII is the big one underpinning a lot of other C++ stuff. But C++ still can't be used in many places that C is, and part of why is baggage that features like RAII require (particularly function overloading and name mangling, even just for destructors alone)... which was carefully considered by the `defer` proposals, such as in N3488 (recently revised to N3687[0]) under section 4, or in other write-ups (including those by that proposal's author) like "Why Not Just Do Simple C++ RAII in C?"[1] and under the "But… What About C++?" section in [2]). In [0] they even directly point to "The Ideal World" (section 4.3) where both `defer` and RAII are available, since as they explain in 4.2, there are benefits to `defer` that RAII misses, and generally both have their uses that the other does not cleanly (if at all) represent! Of course, C++ does still have plenty of nice features that are sorely missing in C (personally longing for the day C gets proper namespaces), so I'm happy we always have it as an option and alternative... but, in turn, I feel the same about C. Sadly isn't as simple to "just use C++" in several domains I care about, let alone dealing with the "what dialect of C++" problem; exceptions or not, etc, etc...
[0]: https://www.open-std.org/JTC1/SC22/WG14/www/docs/n3687.htm [1]: https://thephd.dev/just-put-raii-in-c-bro-please-bro-just-on... [2]: https://thephd.dev/c2y-the-defer-technical-specification-its...
I think in the spirit of C, this should go into the linker, not in the compiler.
Which I find sad actually. The idea of C++ as a superset of C is really powerful, especially when mixing C and C++. A while ago I had a C project (firmware for a microcontroller) and wanted to bake the version and the compilation time into the firmware. I didn't find a way to do this in plain C, but in C++ you can initialize a global struct and it gets statically linked into the output. This didn't even use constexpr, just preprocessor trickery. Then it was just a matter of renaming the c file to cpp and recompiling. I guess you could also do that with C, but there are things like RAII or constexpr or consuming a C++ library that you can't do without.
Not sure what you were running into. I routinely do this just fine.
> This didn't even use constexpr, just preprocessor trickery.
Isn't the preprocessor shared between C and C++?
> in C++ you can initialize a global struct and it gets statically linked into the output
That sounds to be doable just the same in C?
I might be misunderstanding here, but if you are okay with preprocessor trickery, then it's doable.
I do this routinely in the Makefile, which (very tediously) generates a build_info module (header and implementation) that is linked into the final binary: https://github.com/lelanthran/skeleton-c/blob/8e04bed2654dac...
Unless we are speaking about PICs or similar old school 8 and 16 bit CPUs, with compilers like those from MIKROE, there is hardly a platform left were the vendor compiler isn't C and C++ (even if it doesn't go beyond C++11).
And if it must be deployed as freestanding, there are still enough improvements to take advantage of.
In the end it boils down to human factor in most cases, however as Dan Saks puts "If you're arguing, you're losing.", taken from
CppCon 2016: “extern c: Talking to C Programmers about C++”
https://www.youtube.com/watch?v=D7Sd8A6_fYU
If you want to and/or can, then go ahead. This is for those people who either don't want to, or can't, use C++.
Are you suggesting only use C++ over C in all situations?
> Yes.
You cannot imagine any situation where that proposal is a non-starter? Or a deal-breaker?
So you can imagine that C++ instead of C is a deal-breaker or show-stopper in some places, right?
Now that we both agree that there are some communities who won't move off C, do you believe that those communities should never get any upgrade, security fixes, etc?
The fact that go "lifts" the deferred statement out of the block is just another reason in the long list of reasons that go shouldn't exist.
Not only is there no protection against data-races (in a language all about multithreading), basically no static checking for safety, allocation and initialization is easy to mess up, but also defer just doesn't work as it does in C++, Rust, Zig, and any other language that implements similar semantics.
What a joke.
8 more comments available on Hacker News