Skip to content(if available)orjump to list(if available)

Stupid Smart Pointers in C

Stupid Smart Pointers in C

98 comments

·March 17, 2025

rwmj

Really, don't do this, it's a portability and safety nightmare (aside from C not being memory safe already).

C programmers are better off with either of these two techniques:

* Use __attribute__((cleanup)). It's available in GCC and Clang, and we hope will be added to the C spec one day. This is widely used by open source software, eg. in systemd.

* Use a pool allocator like Samba's talloc (https://talloc.samba.org/talloc/doc/html/libtalloc__tutorial...) or Apache's APR.

(I didn't include using reference counting, since although that is also widely used, I've seen it cause so many bugs, plus it interacts badly with how modern CPUs work.)

legohead

My C programs never consumed gigs of memory. So I (like many others I assume) made a memory manager and never freed anything. You'd ask it for memory and it kept a list of various sizes it allocated and returned what you needed to be re-used. Freeing and allocating is slow, and error prone, so just avoid it!

KerrAvon

A venerable and completely reasonable approach for resource-constrained environments and/or programs with very small memory requirements (kilobytes).

legohead

What are your issues with the memory requirements being small? One of the programs was a MUD that consumed a couple hundred megabytes, and I never had issues with it.

I mentioned gigabytes because of how mine specifically worked. It allocated chunks in powers of 2, so there was some % of memory that wasn't being used. For instance, If you only need 20 bytes for a string, you got back a pointer for a chunk of 32 bytes. Being just a game, and side project, I never gave it much thought, so I'm curious to hear your input.

adamrezich

What makes you think that this approach is only useful for resource-constrained circumstances?

masklinn

> we hope will be added to the C spec one day

defer seems to be making significant progress (having a passionate and motivated advocate in Meneide, and a full TS)

rwmj

Last time I looked this was (golang-like) function scoped, not { } scoped, which means it's a bad idea. My feedback was the committee should simply standardize the existing attribute / behaviour, as that is widely used already.

(EDIT: I'm wrong, see reply)

masklinn

> Last time I looked this was (golang-like) function scoped, not { } scoped, which means it's a bad idea.

Might have been the previous attempt from years ago, because being block scoped (unlike go) literally has its own section in https://thephd.dev/c2y-the-defer-technical-specification-its...

EPWN3D

defer is nice, but I really want the cleanup attribute since it could in theory by applied to the return type of a function. In other words you could have malloc return a pointer with the cleanup attribute that automatically frees it at end of scope if it's non-NULL. (And if you want to persist the pointer just assign to a different variable and zero out the one malloc gave you.)

masklinn

> In other words you could have malloc return a pointer with the cleanup attribute that automatically frees it at end of scope if it's non-NULL.

That is not, as far as I know, how __attribute__((cleanup)) works. It just invokes the callback when the value goes out of scope. So you can't have malloc return an implicitly cleanup'd pointer unless malloc is a macro, in which case you can do the same with a defer block.

maccard

> It's available in GCC and Clang, and we hope will be added to the C spec one day. This is widely used by open source software, eg. in systemd.

It’s odd that the suggestion for a feature lacking in C is to use a non standard but well used supported path. c’s main selling point (IMO) is that it _is_ a standard, and relying on compiler vendor extensions kind of defeats the purpose of that.

rwmj

It's so widely used by OS software that you're likely using already, that it's unlikely to be removed and much more likely to be standardized. This is in fact how standardization ought to work - standardize the proven best practices.

maccard

I agree. But if we follow that logic then any compiler specific feature of either or clang is fair game, even if it’s not standard. MSVC doesn’t support it when compiling in C mode, for example.

dietr1ch

> relying on compiler vendor extensions kind of defeats the purpose of that.

Let's be honest, how many compilers are available, and how many of those would you actually use?

The answer isn't more than 4 and the 2 compilers you are most likely to use among those already support this and probably won't stop supporting without a good alternative.

I like standardisation, but you have to be realistic when it helps you without a large real cost other than fighting your ideals for getting this into the standard first.

amjoshuamichael

For me, the point of writing something in C is portability. There were C compilers 30 years ago, there are C compilers now, and there will almost certainly be C compilers 30 years from now. If I want to write a good, portable library that's going to be useful for a long time, I'll do it in C. This is, at least, the standard in many gamedev circles (see: libsdl, libfreetype, the stb_* libraries). Under that expectation, I write to a standard, not a compiler.

DanielHB

There are quite a lot of embedded code that relies on obscure C compilers created and maintained by the CPU manufacturer.

But then again you are probably not doing a whole lot of heap management in embedded code.

maccard

If this is the argument then the actual standardisation is useless. I primarily use windows so I’m affected by one of the major compilers that doesn’t support this feature. This is no different to saying “chrome supports feature X, and realistically has y% market share so don’t let the fact that other browsers exist get in the way”.

Call it a posix extension, fair enough. But if your reason for writing C is that it’s portable, don’t go relying on non portable vendor specific extensions.

flohofwoe

Standard C is only the least common denominator that compiler vendors agreed on, and the C standard committee works 'reactively' by mostly standardizing features that have been in common use as non-standard extensions - sometimes for decades before standardization happens (this is probably the main difference to the C++ committee).

The *actual* power and flexibility of C lies in the non-standard, vendor-specific language extensions.

pajko

There's a complete implementation available at https://github.com/Snaipe/libcsptr

null

[deleted]

MrBuddyCasino

> __attribute__((cleanup))

Interesting. I'm not very proficient in C, this looks like some sort of finalizers for local variables?

rwmj

Correct. You can use it in a simple way to free memory, but we've also used it to create scoped locks[1].

This being C, it's not without its problems. You cannot use it for values that you want to return from the function (as you don't want those to be freed), so any such variables cannot be automatically cleaned up on error paths either. Also there's no automated checking (it's not Rust!)

Note it's {...} scoped, not function scoped, which makes it more useful than Golang's defer.

[1] https://gitlab.com/nbdkit/nbdkit/-/blob/8b36e5a2ea331eed2a73...

NekkoDroid

> You cannot use it for values that you want to return from the function

I would say this is only half true. With some macro magic you can actually also return the values :)

https://github.com/systemd/systemd/blob/0201114bb7f347015ed4...

To be fair though, you probably meant without any such shenanigans.

fpoling

While Go rules effectively prevents usage of defer in loops, it is useful occasionally to write:

    if complex_nested_condition {
       defer cleanup()   
    }

Tewboo

Smart pointers in C often feel like trying to force a square peg into a round hole. They’re powerful, but without native language support like C++, they can lead to more complexity than they solve.

scoopr

Oh well, maybe we'll soon have `defer`? [0]

[0] https://thephd.dev/c2y-the-defer-technical-specification-its...

usrnm

That's sad. Having migrated from C++ to golang a few years ago, I find defer vastly inferior to C++ destructors. Rust did it right with its drop trait, I think it's a much better approach

fuhsnn

The proposed C defer is scope-based unlike Go. So in the spirit of OP article, you can basically hand roll not only C++ destructor as defer {obj.dtor()} but also Rust Drop as defer {obj.notmoved() ? Drop()}

DanielHB

What do you mean defer isn't scope based in Go?

(not super experienced Go developer)

masklinn

Although that is true, the author has expounded at lengths on the unsuitability of RAII to the C programming langage, and as a big fan of RAII the explanations were convincing.

Dwedit

Even Zig which is extremely against "Hidden control flow" (so no operator overloading, etc.) added in the "defer" feature.

vbezhenar

What's "drop trait" for C? There are no any traits in C.

flohofwoe

IMHO trying to emulate smart pointers in C is fixing a problem that shouldn't exist in the first place, and is also a problem in C++ code that uses smart pointers for memory management of individual objects.

Objects often come in batches of the same type and similar maximum lifetime, so let's make use of that.

Instead of tracking the individual lifetimes of thousands of objects it is often possible to group thousands of objects into just a handful of lifetime buckets.

Then use one arena allocator per lifetime bucket, and at the end of the 'bucket lifetime' discard the entire arena with all items in it (which of course assumes that there are no destructors to be called).

And suddenly you reduced a tricky problem (manually keeping track of thousands of lifetimes) to a trivial problem (manually keeping track of only a handful lifetimes).

And for the doubters: Zig demonstrates quite nicely that this approach works well also for big code bases, at least when the stdlib is built around that idea.

Dwedit

Highjacking the return address can only be done if you know you actually have a return address, and a reliable way to get to that return address. Function inlining can change that, adding local variables could change that, omitting frame pointer, etc.

It would also need to be a function that will truly be implemented as one following the ABI, which usually happens when the function is exported. Often times, internal functions won't follow the platform ABI exactly.

Just changing the compiler version is probably enough to break anything like this.

Save the return address highjacking stuff for assembly code.

---

Meanwhile, I personally have written C code that does mess with the stack pointer. It's GBA homebrew, so the program won't quit or finish execution, and resetting the stack pointer has the effect of giving you a little more stack memory.

pjdesno

Note that this will probably cause branch prediction misses, just like thread switching does - modern CPUs have a return address predictor which is just a simple stack. I don’t think you can avoid this without compiler support.

queuebert

C is the LS engine of programming languages. People love to drop it in and mod it until it blows up.

abcd_f

Hacky and not really fit for production for more reasons than one, but clever and nice nonetheless. Good stuff.

feverzsj

Just C programmer's daily struggle to mimic a fraction of C++.

null

[deleted]

qwertox

Not to mention that any future CPU microcode update released in order to mitigate some serious CVE might break the entire product you've been shipping, just because it relied on some stack manipulation wizardry.

whatsakandr

This article should of had the conclusion of this is why you should use arena allocator.

mac3n

this is way overkill

the way i do this in C looks like

    initialize all resource pointers to NULL;

    attempt all allocations;

    if all pointers are non-NULL, do the thing (typically calling another routine)

    free all non-NULL pointers
realloc(ptr, 0) nicely handles allocations and possible-NULL deallocations

mac3n

if you must have a `free_on_exit()` (for example, if you allocate a variable number of pointers in a loop) then build your own defer stack registering pointers using memory that you allocate