from Hacker News

Stupid Smart Pointers in C

by seansh on 3/17/25, 11:29 AM with 164 comments

  • by rwmj on 3/17/25, 12:14 PM

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

  • by flohofwoe on 3/17/25, 2:17 PM

    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.

  • by PortiaBerries on 3/18/25, 12:26 AM

    Around 1994, when I was a nerdy, homeschooled 8th grader teaching myself coding I came up with something I was inordinately proud of. I had gotten a book on PowerPC assembly so using Metrowerks CodeWarrior on my brand-new PowerMac 7100 I wrote a C function with inlined assembly that I called debugf. As I recall, it had the same signature as printf, called sprintf and then passed that resulting string to DebugStr. But the part I was proud of was that it erased itself from the stack so when the debugger popped up it was pointing to the line where you called debugf. I'm still proud of it :-).
  • by scoopr on 3/17/25, 12:41 PM

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

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

  • by casenmgreen on 3/17/25, 10:29 PM

    I maintain a combined error and resource state per thread.

    It is first argument to all functions.

    If no errors, function proceeds - if error, function instead simply immediately returns.

    When allocating resource, resource is recorded in a btree in the state.

    When in a function an error occurs, error is recorded in state; after this point no code executes, because all code runs only if no errors.

    At end of function is boilerplate error, which is added to error state if an error has occurred. So for example if we try to open file and out of disk, we first get error "fopen failed no disk", then second error "opening file failed", and then all parent functions in the current call stack will submit their errors, and you get a call stack.

    Program then proceeds to exit(), and immediately before exit frees all resources (and in correct order) as recorded in btree, and prints error stack.

  • by Tewboo on 3/17/25, 12:59 PM

    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.
  • by Dwedit on 3/17/25, 2:17 PM

    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.

  • by abcd_f on 3/17/25, 12:41 PM

    Hacky and not really fit for production for more reasons than one, but clever and nice nonetheless. Good stuff.
  • by queuebert on 3/17/25, 1:40 PM

    C is the LS engine of programming languages. People love to drop it in and mod it until it blows up.
  • by p0w3n3d on 3/17/25, 1:15 PM

      I see undefined behaviours. 
      they walk 
      they talk 
      they [0?W0OF??0?r??reeBSD
      they don't know they're undefined.
  • by pjdesno on 3/17/25, 12:10 PM

    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.
  • by qwertox on 3/17/25, 2:31 PM

    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.
  • by lionkor on 3/18/25, 9:44 AM

    > Managing memory in C is difficult and error prone. C++ solves this with smart pointers like std::unique_ptr and std::shared_ptr.

    No, it does not. Smart pointers are useful to help model lifetimes and ownership, but the real killer feature is RAII. Add that to C (standardized) and you can make smart pointers, and any other memory management primitive you need. Smart pointers are not a solution, they are one of many tools enabled by RAII.

  • by PeterWhittaker on 3/17/25, 2:34 PM

    1) 2018

    2) I recently discovered the implementation of free_on_exit won't work if called directly from main if gcc aligns the stack. In this case, main adds padding between the saved eip and the saved ebp, (example). I think this can be fixed some tweaking, and will update this article when it is fixed.

    I do not believe the article was updated, suggesting that the "tweaking" was far more complex than the author expected...

    ...which doesn't surprise me, because the overall tone is one of a clever but far-less-experienced-than-they-think programmer having what they think is a flash of insight and realizing thereby they can solve simply a problem that has plagued the industry and community for decades.

  • by jay-barronville on 3/18/25, 2:52 AM

    Like many C hacks, this is a fun one, but hacks like this should absolutely be avoided for any serious C code–the magic is simply not worth the potential bugs!
  • by kazinator on 3/18/25, 5:06 AM

    This kind of approach of allocating objects to a context which is freed all at once is implemented in GNU obstacks.
  • by mac3n on 3/17/25, 4:16 PM

    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
  • by qalmakka on 3/17/25, 1:01 PM

    Or rather, given that every relevant C compiler is also a c++ compiler, just compile as c++ and use std::unique_ptr? I love C but I just can't understand the mental gymnastics of people that prefer this kind of hacks compared to just using C++
  • by whatsakandr on 3/17/25, 2:01 PM

    This article should of had the conclusion of this is why you should use arena allocator.
  • by nanolith on 3/18/25, 12:27 AM

    It's a clever use of assembler, but in production code, it's much better to use a bounded model checker, like CBMC, to verify memory safety across all possible execution paths.
  • by feverzsj on 3/17/25, 12:49 PM

    Just C programmer's daily struggle to mimic a fraction of C++.
  • by afarah1 on 3/17/25, 1:04 PM

    See also the "arena allocator", which has been discussed here before: https://nullprogram.com/blog/2023/09/27/

    I haven't used it personally yet, but it addresses the same issue with a different approach, also related to stack-like lifetimes.

    I've used simple reference counting before, also somewhat relevant in this context, and which skeeto also has a nice post about: https://nullprogram.com/blog/2015/02/17/

  • by adrianN on 3/17/25, 12:04 PM

    I wonder how that affects compiler optimization
  • by anacrolix on 3/17/25, 10:18 PM

    The naive assumption is that shared_ptr is always better than manual tracking. It's not. Tracking and cleaning up resources individually is a burden at scale.
  • by devit on 3/18/25, 3:58 AM

    That's completely asinine since it can't be made to work properly with inlining (including LTO), architectures that use a shadow stack or don't use a frame pointer, and also ridiculously inefficient and requiring assembly code for all architectures.

    Use C++ or __attribute__((cleanup)) instead.

  • by Joker_vD on 3/17/25, 12:50 PM

    Wow, an actual, purposeful, and quite general return-pointer-smashing gadget, built right into the program itself. Just what any program written in C needs.