Understanding C++ Ownership System
46 points by todsacerdoti 12 hours ago | 69 comments
  • Dwedit 11 hours ago |
    C++: Where you can accidentally deallocate an object that's still in the call stack. (true story)
    • einpoklum 11 hours ago |
      Well, you can also write:

         int x = 123;
         delete &x;
      
      and that would compile. But it's not a very good idea and you should be able to, well, not do that.

      In modern C++, we avoid allocating and deallocating ourselves, as much as possible. But of course, if you jump to arbitrary code, or overwrite something that's due as input for deallocation with the wrong address, or similar shenanigans, then - it could happen.

    • kccqzy 11 hours ago |
      You can also do that intentionally and correctly. After all `delete this;` is a valid statement that can occasionally be useful. That said, I’ve only seen this in old pre-C++11 code that does not adhere to the RAII best practice.
    • HarHarVeryFunny 10 hours ago |
      The trouble with C++ is that it maintains backwards compatibility with C, so every error-prone thing you could do in C, you can still do in C++, even though C++ may have a better way.

      The modern, safest, way to use C++, is to use smart pointers rather than raw pointers, which guarantee that nothing gets deleted until there are no more references to it, and that at that point it will get deleted.

      Of course raw pointers and new/delete, even malloc/free, all have their uses, and without these low level facilities you wouldn't be able to create better alternatives like smart pointers, but use these at your own peril, and don't blame the language if you mess up, when you could have just done it the safe way!

      • aw1621107 10 hours ago |
        > which guarantee that nothing gets deleted until there are no more references to it, and that at that point it will get deleted.

        To be more precise, C++'s smart pointers will ensure something is live while specific kinds of references the smart pointer knows about are around, but they won't (and can't) catch all references. For example, std::unique_ptr ensures that no other std::unique_ptr will own its object and std::shared_ptr will not delete its object while there are other std::shared_ptrs around that point to the same object, but neither can track things like `std::span`/`std::string_view`/other kinds of references into their object.

        • HarHarVeryFunny 9 hours ago |
          I was just talking about ownership and smart pointers.

          C++'s non-owning "view" classes are a different matter, and the issue there isn't ownership but lifetime of the view vs the underlying data the view is referencing (which in case of string_view could be literally anywhere - a local array of char, a malloc'd block of memory, etc!!).

          I'm not a fan of a lot of the (relatively) more recent additions to C++. I think C++14 was about the peak! Given that C++ is meant to be usable as a systems programming language, not just for applications, and given that many new features being added to C++ are really library additions, not language ones, then it's important for the language to include unsafe lower level facilities than can be used for things like that, but actually encouraging application developers to use classes like this that are error-prone by design seems questionable!

          • aw1621107 9 hours ago |
            > I was just talking about ownership and smart pointers.

            Sure, I was just clarifying that "references" in the bit I quoted covers specific things and not everything with reference-like behavior.

            • HarHarVeryFunny 8 hours ago |
              Sure, I should have been clearer that smart pointers only guarantee not to release things until there are no more SMART POINTER references to them.

              As soon as you start mix 'n' matching smart pointers and raw pointers into what they are pointing to, then all bets are off!

        • HarHarVeryFunny 8 hours ago |
          > but neither can track things like `std::span`/`std::string_view`/other kinds of references into their object.

          Sure, and if you get into it there are two issues here.

          1) Smart pointers are a leaky abstraction that still gives you access to the underlying raw pointer if you want, with which you can shoot yourself in the foot.

          2) It's not the smart pointer's fault, but the type you use them to point to might also be a leaky abstraction (e.g. std::string, which again allows access to the underlying raw pointer), or might just be some hot garbage the developer themselves wrote!

        • tialaramex 7 hours ago |
          > std::unique_ptr ensures that no other std::unique_ptr will own its object

          Literally all std::unique_ptr does is wrap the pointer. If we make two unique_ptrs which both think they own object X then not only will they both exist and point at X (so they don't actually ensure they're unique) they'll also both try to destroy X when they disenage e.g. at the end of a scope, leading to a double free or similar.

          Uniqueness is a property you are promising, not one they're granting.

          I expect you knew this, but it's worth underscoring that there is no magic here. This really is doing only the very simplest thing you could have imagined, it's a pointer but as an object, a "smart pointer" mostly in the same sense that if I add a cheap WiFi chip to this refrigerator and double the price now it's a "smart fridge".

          • aw1621107 6 hours ago |
            Of course, you're right and my attempt to add precision was itself insufficiently precise. Hoisted by my own petard :(
          • HarHarVeryFunny 5 hours ago |
            It's really asking for trouble if you view it that way - that you are going to allocate something, then uniquely pass it to a single unique_ptr, or shared_ptr, to manage and release. Better really to use std::make_unique() and std::make_shared().

            The smartness, limited though it may be, is just that since it "knows" it's the owner, it knows that it's responsible for releasing it at the appropriate time (reference counted in the case of a shared pointer).

            A lot of the value isn't just the automatic release, but that these classes, unique_ptr, and shared_ptr, are built to enforce their semantics, such as literally not being able to copy a unique_ptr, but are able to move it, and copying a shared_ptr being allowed and increasing the reference count of the object.

            Yes - they are just simple classes, but using them as intended does avoid a lot of common programmer memory management mistakes, and highly adds to the readability of your program, and ability to reason about ownership, since use of these types rather than raw pointers in effect documents who the owner(s) are, and enforces that this documentation is the reality.

      • simonask 9 hours ago |
        The trouble with C++ is that it maintains backward compatibility with C++.
        • HarHarVeryFunny 8 hours ago |
          I suppose so, but I think it's relatively rare for languages to just drop support for older features and force developers to rewrite code using newer mechanisms.

          Python 2 to 3 is a good example of what can be expected to happen - very slow adoption of the new version since companies may just not have the resources or desire to rewrite existing code that is running without problems.

          • tialaramex 8 hours ago |
            Late in the C++ 20 timeline P1881 Epochs were proposed. Similar to Rust's editions, which at that time had been tried in anger only once (2018 Edition) this proposed that there should be a way for C++ to evolve, forbidding obsolete syntax in newer projects rather than just growing forever like cancer.

            Epochs was given the usual WG21 treatment and that's the end of that. Rust shipped 2021 Edition, and 2024 Edition, and I see no reason to think 2027 Edition won't happen.

            The current iteration of Bjarne's "Profiles" idea is in a similar ballpark though it got there via a very different route. This time because it will aid safety to outlaw things that are now considered a bad idea. If this goes anywhere its nearest ship vehicle is C++ 29.

            Now, Python 2 to Python 3 did take a few years, maybe a decade. But just shipping the mechanism to reform C++ looks likely to take at least nine years. Not the reform, just the mechanism to enable it.

            • pjmlp 18 minutes ago |
              Meanwhile editions still don't have an answer for semantics changes exposed on public APIs for crates, each compiled on its own edition.

              So what does the compiler chose, the edition of the caller, or the caller?

          • aw1621107 5 hours ago |
            > Python 2 to 3 is a good example of what can be expected to happen

            People keep bringing this up when discussing backwards compatibility breaks, but I think the conclusion should be a bit more nuanced than just "backwards compatibility break <=> years (decades?) of pain".

            IMHO, the problem was the backwards compatibility break coupled with the inability to use Python 2 code from 3 and vice versa. This meant that not only did you need to migrate your own code, but you also needed everything you depend on to also support Python 3. This applied in reverse as well - if you as a library developer naively upgraded to Python 3, that left your Python 2 consumers behind.

            Obviously the migration story got better over time with improvements to 2to3, six, etc., that allowed a single codebase to work under both Python 2 and 3, but I think the big takeaway here is that backwards compatibility breaks can be made much more friendly as long as upgrades can be performed independently of each other.

      • Conscat 8 hours ago |
        The modern safest way to use C++ involves lifetime annotations and ownership annotations run under multiple built-in Clang constraint solvers, but this isn't what most users do.
  • jesse__ 11 hours ago |
    Does anyone reading this have links to people who have written specifically about a C++ ownership model that rejects the smart_ptr/RAII/friends model in favor of an ownership model that embraces bulk allocations, arenas, freelists, etc? I know there are groups of highly productive programmers that feel the traditional C++ ownership model is hot garbage, and I'd love a resource that puts down specific arguments against it, but I've never come across one myself.

    Edit: clarity

    • rubymamis 11 hours ago |
      I'm interested in the same! There are plenty of resources for C[1][2]. I just looked into my old notes and found a post for C++[3].

      [1] https://btmc.substack.com/p/memory-unsafety-is-an-attitude-p...

      [2] https://www.gingerbill.org/series/memory-allocation-strategi...

      [3] https://dmitrysoshnikov.com/compilers/writing-a-pool-allocat...

      • jesse__ 10 hours ago |
        Nice, thanks. I haven't read those gingerbill ones, I'll take a look :D
    • otherjason 11 hours ago |
      What makes you think that RAII- and arena-based strategies are in tension with one another? RAII and smart pointers are more related to the ownership and resource management model. Allocating items in bulk or from arenas is more about where the underlying resources and/or memory come from. These concepts can certainly be used in tandem. What is the substance of the argument that RAII, etc. are "hot garbage?"
      • einpoklum 10 hours ago |
        In my library [1], wrapping the CUDA APIs in modern C++, I do allocations which are not exactly from an arena, but something in that neighborhood - memory spaces on context on GPU devices.

        Unlike the GP suggests, and like you suggest, I have indeed embraced RAII in the library - generally, not just w.r.t. memory allocation. I have not, however, replicated that idioms of the standard library. So, for example:

        * My allocations are never typed.

        * The allocation 'primitives' return a memory_region type - essentially a pointer and a size; I discourage the user from manipulating raw pointers.

        * Instead of unique_ptr's, I encourage the use of unique_span's: owning, typed, lightweight-ish containers - like a fusion of std::span<T> and std::unique_ptr<T[]> .

        I wonder if that might seem less annoying to GP.

        ---

        [1] : https://github.com/eyalroz/cuda-api-wrappers/

      • jesse__ 10 hours ago |
        In reverse order they were asked ..

        The best argument I've ever come across against using RAII is that you end up with these nests of objects pointing to one another, and if something fails, the cleanup code can really only do one thing, which is unwind and deallocate (or whatever the cleanup path is). This structure, generally, precludes the possibility of context dependent resource re-usage on initialization failure, or on deallocation, because you kind of have to have only one deallocation path. Obviously, you could imagine supporting in an RAII context, but, the point is that you probably have to put a fair bit of conscious effort into doing that, whereas if you have a less .. rigid.. ownership model, it becomes completely trivial.

        I agree that the allocation model and ownership model are independent concepts. I mentioned arena allocation because the people I know that reject the traditional C++ ownership model generally tend to favor arenas, scratch space, freelists, etc. I'm specifically interested in an ownership model that works with arenas, and tracks ownership of the group of allocations, as opposed to the typical case we think about with RAII where we track ownership of individual allocations.

        • HarHarVeryFunny 8 hours ago |
          That "nest of objects point to each other" makes no sense ... RAII is just a technique where you choose to tie resource management to the lifetime of an object (i.e. acquire in constructor, release in destructor).

          If an exception gets thrown, causing your RAII object scope to be exited, then no problem - the object destructor gets called and the resource gets released (this is the entire point of RAII - to make resource allocation and deallocation automatic and bullet-proof).

          If you are creating spaghetti-like cross-referencing data structures, then that is either poor design or something you are doing deliberately because you need it. In either case, it has nothing to do with RAII, and RAII will work as normal and release resources when the managing object is destroyed (e.g. by going out of scope).

          RAII could obviously be used to allocate/free a resource (e.g. temp buffer) from a free list, but is not really relevant to arena allocation unless you are talking about managing the allocation/release of the entire arena. The whole point of an arena allocator is the exact opposite of RAII - you are deliberately disconnecting individual item allocation from release so that you can instead do a more efficient bulk (entire arena) release.

          • jesse__ 7 hours ago |
            Another commentor succinctly pointed out one argument against RAII+friends is that it encourages thinking about single objects, as opposed to bulk processing.

            In many contexts, the common case is in fact bulk processing, and programming things with the assumption that everything is a single, discrete element creates several problems, mostly wrt. performance, but also maintainability. [1][2]

            > The whole point of an arena allocator is the exact opposite of RAII

            Yes, agreed. And the internet is rife with people yelling about just how great RAII is, but comparatively few people have written specifically about it's failings, and alternatives, which is what I'm asking about today.

            [1] https://www.youtube.com/watch?v=tD5NrevFtbU

            [2] https://www.youtube.com/watch?v=rX0ItVEVjHc&t=2252

            • vacuity 6 hours ago |
              I don't think the "complex web of objects to be deallocated" scenario is usually a problem, but I generally agree with your points. As always, careful design and control of the software is important. Abstractions are limited; spend them carefully.
            • HarHarVeryFunny 5 hours ago |
              > one argument against RAII+friends is that it encourages thinking about single objects, as opposed to bulk processing.

              RAII is just a way to guarantee correctness by tying a resource's lifetime to that of an object, with resource release guaranteed to happen. It is literally just saying that you will manage your resource (a completely abstract concept - doesn't have to be memory) by initializing it in an objects constructor and release it in the destructor.

              Use of RAII as a technique is completely orthogonal to what your program is doing. Maybe you have a use for it in a few places, maybe you don't. It's got nothing to do with whether you are doing "bulk procesing" or not, and everything to do with whether you have resources whose usage you want to align to the lifetime of an object (e.g this function will use this file/mutex/buffer, then must release it before it exits).

        • mgaunard 4 hours ago |
          There are plenty of people who use RAII with arenas for nested group of objects.

          Bloomberg for example had a strong focus on that, and they enhanced the allocator model quite significantly to be able to standardize this. This was the reason for stateful allocators, scoped allocators, uses-allocator construction and polymorphic memory resources.

    • aw1621107 10 hours ago |
      > explicitly rejects the smart_ptr/RAII/friends model in favor of bulk allocations, arenas, freelists, etc?

      These aren't mutually exclusive; you can use the former to manage the latter, after all.

      > I know there are groups of highly productive programmers that feel the traditional C++ ownership model is hot garbage

      I'm not aware of links off the top of my head, but I can try to summarize the argument.

      From my understanding, the argument against RAII/etc. has more to do with the mindset it supposedly encourages more than the concept itself - that RAII and friends makes it easy to think more in terms of individual objects/elements/etc. instead of batches/groups, and as a result programmers tend to follow the easy path which results in less performant/more complex code. By not providing such a feature, so the argument goes, programmers no longer have access to a feature which makes less-efficient programming patterns easy and so batched/grouped management of resources becomes more visible as an alternative.

      • jesse__ 10 hours ago |
        Agreed. I guess I'm interested in anyone that's specifically written about ownership strategies that lean into the group allocation thing.
    • nwlieb 10 hours ago |
      Yes: https://m.youtube.com/watch?v=xt1KNDmOYqA

      Title: “ Casey Muratori | Smart-Pointers, RAII, ZII? Becoming an N+2 programmer”

      • jesse__ 10 hours ago |
        Good one. I was blessed to have the opportunity to watch that one live, on stream. It's always stuck with me and, now that I think about it, is the best resource I know of that puts those ideas into words/writing.
    • verall 10 hours ago |
      If you have requirements for high performance then the traditional C++ "ownership model" (I would say a better description is "ownership strategy") is definitely "slow". It's pretty "safe" in that you usually aren't going to leak a bunch with it but bull allocations, arenas, and freelists are all potentially faster. And you wouldn't use them if they were slower since they're (usually) more to deal with.

      But even in software using these strategies, they probably will be using different ownership strategies in different parts of the code. Once you're writing high performance code, you will use specific strategies that give you the best results. But it's completey domain specific.

    • GrowingSideways 10 hours ago |
      Such a model likely would not be referred to as "ownership". This is a relatively recent metaphor for memory management that came well after the concepts you mentioned. The fact that such a metaphor is core to rust's memory model is no coincidence.
    • HarHarVeryFunny 10 hours ago |
      Those types of allocation technique were common back in the day for efficiency reasons, maybe still relevant for things like embedded programming where you need to be more careful about memory usage and timing, but I would say that nowadays for normal application usage you are better off using smart pointers.

      It's not a matter of one being strictly better than the other, but rather about using the right tool for the job.

      • jesse__ 9 hours ago |
        Many soft-realtime systems make use of these techniques, specifically 3D graphics and game engines.
      • dundarious 8 hours ago |
        I disagree, as group lifetimes are conceptually and architecturally often easier and simpler than having lots of individual lifetimes managed by smart pointers. And sure, you can often slap shared_ptr around the place, or hopefully a less lazy smart ptr choice, but it makes the code harder to understand by obscuring ownership rather than eliminating it as a concern.
        • HarHarVeryFunny 8 hours ago |
          I don't understand why you see it that way... Whether using an arena allocator or smart pointer, in both cases you need to allocate the individual object (p = arena_allocate(arena, ...), or p = std::make_unique<T>()). In the case of a smart pointer that is all you have to do - deallocation will be automatic. In the case of the arena allocator you will also need to deallocate the entire arena at the appropriate time.

          How are you conceptualizing this that the arena allocator is simpler?

          How are you conceptualizing smart pointers as "obscuring" ownership, when the entire point of them is to make ownership explicit! The smart pointer IS the owner!

    • edflsafoiewq 8 minutes ago |
  • einpoklum 11 hours ago |
    The title reminds of this:

    https://youtu.be/TGfQu0bQTKc?si=7TiDRic6LaWI1Xpc&t=70

    "In Rust you need to worry about borrowing. In C++ you don't have to worry about borrowing; in C++ you have to worry about ownership, which is an old concept..." :-P

  • jurschreuder 10 hours ago |
    I don't know why people use 'new' and 'delete' in all the examples how memory in C++ works because you never normally use them during coding only if you want to make your own container which you might do once to learn about the internals.

    C++ by default creates objects by value (opposed to any other language) and when the variable goes out of scope the variable is cleaned up.

    'new' you use when you want to make a global raw pointer outside of the normal memory system is how I would see it. You really never use it normally at least I don't.

    A good rule of thumb is not to use 'new'.

    • vlovich123 10 hours ago |
      And yet, I interviewed 10 people easily where I was using new and delete in the example code and only one person asked "hey - can we use unique_ptr?".
      • oxag3n 10 hours ago |
        Ownership problems with pointer/references don't end with allocation.

        A codebase can use only std::make_unique() to allocate heap, and still pass around raw pointers to that memory (std::unique_ptr::get()).

        The real problem is data model relying on manual lifetime synchronization, e.g. pass raw pointer to my unique_ptr to another thread, because this thread joins that thread before existing and killing the unique_ptr.

        • vlovich123 3 hours ago |
          I don’t disagree. That’s why I don’t write C++ anymore. It’s a masochistic language and trying to do it in a team environment with people who do understand how to do it properly is a mess let alone adding in people who don’t know.
      • mackeye 9 hours ago |
        many schools (like mine) don't teach unique pointers in the pure "programming" class sequence, but offer a primer in advanced classes where c++ happens to be used, with the intent to teach manual memory management for a clearer transition to e.g. upper-levels which use c.
      • johannes1234321 9 hours ago |
        Well in interviews this is tricky. Sometimes the interviewer wants to see I can new/delete properly, sometimes this tells me "well, if that's the style they are using I better go elsewhere"

        If it's done as part of a "here is legacy code, suggest ways to improve it" question one should point it out, though.

        • vlovich123 3 hours ago |
          Don’t worry. The question was super basic and almost every candidate still failed to mention that you need to delete in the destructor, that copying requires a new allocation or that you also need to define the move constructor. It made me sad about the state of C++ developers and thankful that Rust makes such mistakes impossible.
    • mikepurvis 10 hours ago |
      Yup, just emplace the object directly into the container, or at worst create it by value and then add it to the container with std::move.
    • unclad5968 10 hours ago |
      It just makes for an easily understandable example. I don't think the post is advocating for the use of new/delete over smart pointers.
    • duped 9 hours ago |
      People use `new` and `delete` when explaining memory in C++ because those are the language primitives for allocating and releasing memory in C++.

      That rule of thumb is only a useful rule if you don't care about how memory works and are comfortable with abstractions like RAII. That's fine for lots of real code but dismissing `new` and `delete` on principle is not interesting or productive for any discussion.

      • tialaramex 7 hours ago |
        Also they're operators.

        I understand C++ has a lot of operators which are variously reserved but not standardized ("asm") largely obsolete but still needed because of perverse C programmers ("goto") still reserved long after their usefulness subsided ("register") or for ideas that are now abandoned ("synchronized") not to mention all its primitive types ("double", "signed", "long", "short", "char8_t") and redundant non-textual operators given ASCII names like ("and_eq", "bitand", "xor")

        But it also has dozens, like new and delete which look like features you'd want. So kinda makes sense to at least mention them in this context.

      • jurschreuder 3 hours ago |
        No the primitives are:

        {

          // allocate
        
          auto my_obj = MyObj{}
        
        } // released
    • johnnyanmac 9 hours ago |
      Yes, and no?

      In production, odds are you are relying on allocators or containers others already wrote. You coming in in 2026 may not ever use the keywords directly, but you'll either be using abstractions that handle that for you (be it STL or something internal) or using some custom allocation call referring to memory already allocated.

      But yes, I'd say a more general rule is "allocate with caution".

    • mgaunard 8 hours ago |
      even when you write your own container, you do not use new and delete.
      • tialaramex 8 hours ago |
        Are you sure? It seems as though ultimately Microsoft's STL for example ends up calling std::allocator's allocate function which uses the new operator.
        • mgaunard 4 hours ago |
          you would use "operator new" (allocates memory only) not "new" (allocates and constructs, and more if using the new[] variant)
      • edflsafoiewq 22 minutes ago |
        You might use placement new though.
    • ranit 8 hours ago |
      > I don't know why people use 'new' and 'delete' in all the examples ...

      Why? Because the blog post is titled "Understanding C++ Ownership System".

      • jurschreuder 3 hours ago |
        He's making it massively more complex than it actually is

        { // this scope is owner

          // allocate
        
          auto my_obj = MyObj{};
        
          // this function scope does not have ownership of my_obj, should take (const MyObj& obj) const reference as parameter
        
          do_something(my_obj);
        
        } // memory is released
      • bulbar 2 hours ago |
        Such article can end up with a 'false balance' bias by introducing and showing a method one should avoid to motivate the solution. What some people learn is "there are two options".

        Maybe it works be better to start with "that's how we do it" and only afterwards following up with "and that's why".

    • pjmlp 22 minutes ago |
      Unfortunately they are all over the place on corporate code.
  • dpsych 10 hours ago |
    I think in the `Move` section the delete[] should be delete[] old_buffer; rather than new_buffer;
  • cocoto 10 hours ago |
    Why are some examples full of errors? The `set_vec` method for instance does not bind the reference, you can't change the reference itself... so the code would simply copy the vector and there would be no dangling reference... And `B` is missing a constructor since the default constructor would be ill-formed (you can't default initialize a reference).

    Anyway the article is quite approachable, do not take my criticism to shy away from writing!

    • pixelesque 9 hours ago |
      Yeah, that example's totally wrong, as you say, the std::vector<int> would get copied by value, so there'd be no issue at all.
    • tialaramex 7 hours ago |
      I strongly encourage people writing textual examples in 2026 to use Compiler Explorer.

      https://cpp.godbolt.org/

      Matt Godbolt's tool lets your reader play with your examples and learn more about what's going on. As a bonus, if it doesn't compile and work in Compiler Explorer now you know early before you hit "publish". It's the same reason you should run a spellchecker, raweht thun jstu hope forr th bess

  • vqsubu16 9 hours ago |
    Why there is the calling of "read(buffer.get());" in the first example (inside of the 'while' loop)?

    It is a 'char *buffer' type, unless I'm mistaken raw pointers don't have methods/member functions?

    • dundarious 9 hours ago |
      copy-paste error given the next example uses a smart ptr type that has a .get() to get the actual pointer.
  • fleshmonad 8 hours ago |
    Author currently unemployed type post to state in zoomerspeech