Ownership in the Type System
A raw pointer doesn't say who owns it. A smart pointer does, in the type itself, checked by the compiler. This post is about how C++ turns "who owns this, and when does it die?" from a comment into a contract, using three tools: unique_ptr, shared_ptr, and weak_ptr. We start with the idea that makes all of them work (RAII), then meet each one next to its raw-pointer equivalent, so the upgrade is visible.
The raw-pointer problem, restated
Everything we've covered so far in this series has been about raw pointers. They're powerful, flexible, and completely silent about ownership. Look at this function signature:
void process(Widget* w);
Does process take ownership of the widget and delete it when done? Does it borrow the widget briefly and leave the caller to clean up? Is it storing the pointer somewhere, to be used later? The signature doesn't say. You have to read the implementation or trust the documentation, and if either of them lies, you get a memory bug at some undetermined point in the future.
Every major memory-safety failure in C-family languages comes down to this silence. Memory leaks happen when nobody remembers they're the owner. Double-frees happen when two people think they are. Dangling pointers happen when a non-owner keeps using memory after the owner freed it. Use-after-free is the general name for the whole family.
Smart pointers fix this by putting the ownership story into the type. Three types, three stories: "I'm the only owner" (unique_ptr), "we share ownership" (shared_ptr), and "I'm watching but don't own" (weak_ptr). Pick the right one and the compiler enforces the rules of that story for free.
The engine underneath: RAII
Before we meet the smart pointers themselves, we need the idea they rest on: RAII, which stands for "Resource Acquisition Is Initialisation." It's one of C++'s core ideas, and it's simpler than the name makes it sound.
The rule: tie a resource's lifetime to the lifetime of an object on the stack. When the object is created, it acquires the resource. When the object goes out of scope, its destructor releases it. Because C++ guarantees that destructors run when scope ends (even if the scope exits via an exception), you get automatic cleanup without having to remember anything.
Here's RAII for dynamic memory, written by hand:
class IntHolder { int* ptr; public: IntHolder(int v) : ptr(new int(v)) {} // acquire ~IntHolder() { delete ptr; } // release int get() const { return *ptr; } }; void example() { IntHolder h(42); // new int(42) happens here std::cout << h.get(); } // h's destructor runs here, delete ptr; done.
Notice what we've gained: there's no way for example to leak the int. Whether it returns normally, returns early, or throws an exception, h's destructor runs and cleans up. The memory management is automatic because we embedded it into an object whose lifetime the compiler already manages.
But we've also created a new problem. What happens if we copy h?
IntHolder a(42); IntHolder b = a; // default copy: b.ptr = a.ptr (same address!) // when both go out of scope: double-free UB
The default copy constructor copies the pointer itself, not what it points at. Now a and b both hold the same address, and when both destructors run, you delete the same memory twice. This is why, whenever a class manages a resource, you have to think carefully about what copies, moves, and assignments mean. That discipline is called the Rule of Five: if you write (or need) any of destructor, copy constructor, copy assignment, move constructor, or move assignment, you probably need to think about all five.
Move semantics, briefly
C++11 added move semantics, which is the other half of the smart-pointer story. A move is like a copy, except the original gets emptied out in the process. Instead of duplicating the resource, you transfer it. The source ends in a valid-but-empty state; the destination has everything.
// Pseudo-code for what "move" does with a pointer: b.ptr = a.ptr; // b takes over a.ptr = nullptr; // a is now empty, destructor will do nothing
Now when a and b go out of scope, b's destructor frees the memory (once), and a's destructor sees nullptr and does nothing. No double-free, no leak. The pattern is enforced by the compiler: some objects are movable but not copyable, and the type system won't let you accidentally copy them. This is exactly what unique_ptr is.
The stack manages itself; everything on it gets destroyed in reverse creation order when scope ends. The heap does not. A raw-pointer function has to explicitly delete before every exit path, and if anything throws, the cleanup gets skipped. A unique_ptr on the stack embeds the delete into its destructor, so any way the scope ends, the heap memory is freed.
unique_ptr: exactly one owner
The most common smart pointer, and the default one. A unique_ptr<T> holds a pointer to a T on the heap, and guarantees that exactly one unique_ptr owns that T at any time. When the unique_ptr goes out of scope, it deletes the T. When you want to transfer ownership somewhere else, you move the unique_ptr; you can't copy it.
Here it is next to the raw-pointer equivalent:
The raw version is longer and buggier. Every early return needs a delete. Forget one, leak. Add an exception path, leak. The unique_ptr version compiles down to roughly the same assembly but deletes the Widget automatically on every exit path.
The most important rule: always prefer std::make_unique<T>(args...) over std::unique_ptr<T>(new T(args...)). It's shorter, it's exception-safe in ways the new version isn't, and it's the idiom the compiler and reader expect. Use make_unique.
Ownership transfer via move
You can't copy a unique_ptr. You can only move it. That's the language enforcing "exactly one owner":
std::unique_ptr<Widget> a = std::make_unique<Widget>(); std::unique_ptr<Widget> b = a; // ERROR: can't copy std::unique_ptr<Widget> c = std::move(a); // OK: ownership transferred // a is now empty (nullptr) if (a) std::cout << "still owns"; // won't print c->do_work(); // fine, c is the owner now
This is the type system making "exactly one owner" an invariant. If you tried to copy, the compiler refuses. If you move, the language guarantees the source is left empty so only the destination owns. No runtime check, no reference count, no overhead beyond the raw pointer itself.
unique_ptr in function signatures
The real power of unique_ptr shows up in function signatures. Each way of using it communicates exactly one ownership intent:
// "I take ownership. The caller's unique_ptr is emptied." void consume(std::unique_ptr<Widget> w); // "I borrow briefly. Caller keeps owning, and I promise not to delete." void inspect(const Widget* w); // or: void inspect(const Widget& w); // "I create and return ownership to the caller." std::unique_ptr<Widget> make_widget();
Contrast with the raw-pointer world, where void f(Widget* w) tells you nothing: takes ownership? borrows? stores? The unique_ptr version says the answer in the type. This is why modern C++ style guides (Google, LLVM, Abseil, the C++ Core Guidelines) all push hard for smart pointers in signatures.
unique_ptr is the default smart pointer. Use it whenever one piece of code owns a heap allocation from cradle to grave. Reach for something else only when you actually have to share.shared_ptr: counted sharing
Sometimes a single owner isn't the right story. Multiple parts of your program legitimately share a resource, and it should live as long as any of them still need it. That's what shared_ptr is for.
Internally, a shared_ptr<T> holds two things: a pointer to the T, and a pointer to a small control block that counts references. Every time you copy the shared_ptr, the count goes up. Every time one is destroyed, the count goes down. When the count hits zero, the T is deleted. It's deterministic, automatic reference counting in a box.
The raw version is what people wrote before shared_ptr existed (and what reference-counted types in older codebases still do). Every caller has to pair every addref with a release, and one mistake leaks or double-frees. The shared_ptr version ties the ref-count to construction and destruction, so it can't be forgotten.
As with unique, always prefer std::make_shared<T>(args...) over std::shared_ptr<T>(new T(args...)). Here there's an extra reason: make_shared allocates the object and the control block in a single allocation, which is faster and uses less memory than two separate news.
When to reach for shared_ptr
A good rule of thumb: if you can state "this one thing owns this" without lying, use unique_ptr. Only reach for shared_ptr when there's genuinely no single owner: the object participates in a graph, or it's cached and might be used from several independent code paths, or its lifetime is determined by "last user wins" across threads.
The cost of shared_ptr is real and worth naming:
- Memory: a control block per allocation (16 bytes on typical 64-bit systems), and every
shared_ptris twice the size of a raw pointer. - Time: every copy and destruction involves atomic increment/decrement of the ref count. Atomics are cheap but not free, and in hot paths they add up.
- Cycles: two
shared_ptrs pointing at each other never hit zero, so neither is ever deleted. This is the classicshared_ptrleak. The fix isweak_ptr, which is next.
The cycle problem
Here's the leak that motivates the third smart pointer. Consider two nodes that point at each other:
struct Node { std::shared_ptr<Node> neighbour; }; auto a = std::make_shared<Node>(); auto b = std::make_shared<Node>(); a->neighbour = b; // b's refcount: 2 b->neighbour = a; // a's refcount: 2 // End of scope. Local a dies (refcount drops to 1, still > 0). // Local b dies (refcount drops to 1, still > 0). // Neither is ever deleted. Memory leaked, no crash.
This is a memory leak caused by shared_ptr being too eager to keep things alive. The two nodes are keeping each other alive through their mutual references, and nothing outside the cycle holds a pointer to either. The fix is to break the cycle: exactly one direction should be a non-owning reference. That's what weak_ptr is for.
shared_ptr buys flexibility with atomic overhead and cycle risk. Use it when sharing is real, not when you couldn't be bothered to think about ownership.The first scenario shows normal sharing: the object dies when the last owner releases it. The second shows the cycle: both nodes end with refcount 1, nothing hits zero, memory leaks silently. The third replaces one edge with weak_ptr, which doesn't increment the refcount, so when the strong refs all release, the count hits zero and cleanup happens properly.
weak_ptr: observe without owning
A weak_ptr<T> is a non-owning observer of a shared_ptr-managed object. It holds a pointer to the control block, can tell you whether the object is still alive, and can give you a shared_ptr if it is. But it does not contribute to the reference count. The object can die while weak_ptrs are still watching; the weak_ptrs will detect that.
Here's its shape:
auto s = std::make_shared<Widget>(); // refcount: 1 std::weak_ptr<Widget> w = s; // refcount: still 1 (weak doesn't count) // Later, to use the widget, ask the weak_ptr to promote to shared_ptr: if (auto locked = w.lock()) { // nullptr if Widget is gone locked->do_work(); // refcount temporarily up to 2 } // back to 1 s.reset(); // refcount hits 0, Widget deleted if (w.expired()) std::cout << "Widget's gone\n"; // yes it is
Two operations do most of the work. lock() tries to upgrade the weak reference to a strong one; if the object is still alive, you get a valid shared_ptr, otherwise nullptr. expired() checks without upgrading. The check-and-use pattern (if (auto p = w.lock())) is the idiom for "use it if it's still there."
Two things weak_ptr is actually for
1. Breaking cycles. In the Node example above, change one direction to weak_ptr and the cycle disappears:
struct Node { std::shared_ptr<Node> next; // owning forward link std::weak_ptr<Node> prev; // non-owning back-link };
This is the classic recipe for doubly-linked lists, parent/child trees, and any graph where one direction is "the real relationship" and the other is "also nice to have." The shared_ptrs form a DAG of ownership; the weak_ptrs are observers that don't contribute to keeping things alive.
2. Caches and observers. Sometimes you want to remember that you gave out a pointer, but not keep the pointee alive just because you have a record of it. A cache that maps names to shared_ptrs would leak forever (the cache would be the last holder of everything ever inserted); a cache that maps names to weak_ptrs lets objects die naturally when the rest of the program releases them, and the cache quietly forgets them:
std::unordered_map<std::string, std::weak_ptr<Texture>> cache; std::shared_ptr<Texture> load_texture(const std::string& name) { if (auto hit = cache[name].lock()) return hit; // still alive? reuse. auto fresh = std::make_shared<Texture>(name); cache[name] = fresh; // remember, but don't own. return fresh; }
The cache is a lookup helper, not an owner. Textures stay alive while the game is using them, and drop out of memory when nothing else is holding on. Perfect use of weak_ptr.
weak_ptr is how you observe without owning. Use it to break cycles and to hold non-owning references that survive the pointee's death.Putting it together: picking the right one
Three pointers, three stories. The decision is almost mechanical once you ask the right questions.
Start by asking: does this piece of memory need dynamic lifetime at all? If the lifetime is tied to a scope, you don't need any smart pointer. Put the object on the stack, pass it by reference or const-reference, and you're done. Smart pointers are for heap allocations.
If it does need heap lifetime, ask: is there one owner, or several? If one owner, unique_ptr. If genuinely several, shared_ptr.
If there's a shared_ptr, ask: does anything need to observe the object without keeping it alive? Back-references, caches, observers. Those are weak_ptr.
And ask: does a non-owning function need to borrow it briefly? Pass a raw pointer or reference. Raw pointers still exist in modern C++; they just mean "non-owning, borrowed for this call." The smart pointers are for ownership; raw pointers and references are for access.
Four questions, at most. The decision tree isn't deep because the concepts aren't; you almost always want unique_ptr, you sometimes want shared_ptr, you rarely want weak_ptr, and you reach for a raw pointer or reference when the code isn't responsible for the lifetime at all.
Custom deleters and other knobs
A few advanced features worth knowing about, even if you don't use them every day.
Custom deleters. Smart pointers can manage things other than heap memory: file handles, OS resources, arena allocations. You supply a custom deleter, and the smart pointer calls it instead of delete:
// Managing a FILE* from stdio, which needs fclose, not delete: auto closer = [](std::FILE* f) { if (f) std::fclose(f); }; std::unique_ptr<std::FILE, decltype(closer)> f( std::fopen("data.txt", "r"), closer);
This is RAII applied to any resource with a paired acquire/release API. Every C resource-handle pattern can be wrapped in a unique_ptr with a custom deleter, and the result is exception-safe and leak-proof.
Arrays. unique_ptr<T[]> and make_unique<T[]>(n) work for array allocations, calling delete[] correctly. (shared_ptr only got proper array support in C++17 with shared_ptr<T[]>; before that you had to supply a custom deleter.)
enable_shared_from_this. If a class method needs to hand out a shared_ptr to itself (for callback registration, for instance), you cannot just write shared_ptr<Self>(this). That creates a second control block with a refcount of 1, and the object will be double-deleted. The correct pattern is to inherit from std::enable_shared_from_this<Self> and call shared_from_this(), which gives you a shared_ptr sharing the existing control block. It's niche but essential when the shape of the problem demands it.
No standard intrusive pointers. In performance-critical code (game engines, some embedded) you'll see intrusive reference-counted pointers, where the count is stored in the managed object itself rather than in a separate control block. The standard library doesn't ship one; projects use Boost's intrusive_ptr or roll their own.
Common pitfalls
Four traps that catch people even after they know the basic rules.
1. Mixing smart and raw ownership. If you write Widget* raw = smart.get();, the raw is a borrowed pointer. It's fine to pass to functions that take raw parameters. It is not fine to delete it, or to hand it to another smart pointer, or to outlive the original smart pointer. "Get a raw pointer for non-owning access" is the correct use; "get a raw pointer to manage lifetime separately" is a bug.
2. Passing shared_ptr by value everywhere. Every time you pass a shared_ptr by value, that's an atomic increment at the call and an atomic decrement at the return. In a hot path with a pointer that's passed through ten levels of calls, that adds up fast. Pass by const& when the callee doesn't need to extend the lifetime, and by value when it genuinely takes (shared) ownership.
3. Using make_shared for large objects in memory-constrained environments. Because make_shared allocates the object and the control block together, the memory stays allocated until the last weak_ptr is gone, not just the last shared_ptr. For large objects with many observers, this can keep a lot of memory alive. shared_ptr<T>(new T(...)) separates the two allocations; the object can be freed as soon as the last shared_ptr dies, even if weak_ptrs remain. This is one of the few cases where make_shared is the wrong choice.
4. Using shared_ptr as a "default." In codebases that migrated from Java or Python, people sometimes make every owning pointer a shared_ptr out of habit. This is almost always wrong in C++. It costs time, memory, and clarity. The ownership graph of a program should look like a tree or DAG with unique_ptrs; shared_ptr is for the exceptional nodes where sharing is genuinely needed.
A note on performance
A question that comes up often: are smart pointers slower than raw pointers? The short answer is "unique_ptr is basically free; shared_ptr has real cost; weak_ptr is similar to shared_ptr but usually colder."
unique_ptr compiles down to the same code as a raw pointer plus a destructor call at scope exit. No ref count, no atomics, no indirection. Modern compilers are very good at optimising this; there's no measurable overhead versus a well-written raw-pointer program. Use it freely.
shared_ptr has two atomic operations per copy (increment on assignment, decrement on destruction), an extra indirection through the control block on each access, and a larger pointer (16 bytes vs 8 on 64-bit). On a hot loop, this shows up on profiles. On normal code it's invisible.
weak_ptr itself is cheap; its cost is mostly paid when you call lock(), which does an atomic read-and-increment on the control block to make sure the object hasn't been destroyed mid-call. Still cheaper than whatever bug you were about to write instead.
Measure before optimising. The "smart pointers are slow" reputation comes from codebases that reached for shared_ptr as a default; use unique_ptr as the default and the overhead is usually invisible.
Summing up
Raw pointers carry no ownership information. Smart pointers put ownership in the type system, where the compiler can check it.
unique_ptr<T> is "exactly one owner." Move-only, zero runtime overhead, the default smart pointer. Create with std::make_unique<T>(args...), transfer ownership with std::move, never copy.
shared_ptr<T> is "shared ownership with a reference count." Use when no single owner exists, with awareness of the atomic overhead and cycle risk. Create with std::make_shared<T>(args...) for efficiency.
weak_ptr<T> is "observe without owning." Use to break shared_ptr cycles and to hold caching references that let their pointees die. Access via lock(), which returns a shared_ptr or nullptr.
Underneath them all is RAII, C++'s core pattern of tying resource lifetime to stack-object lifetime, combined with move semantics to transfer ownership without duplicating the resource. Once those two ideas click, smart pointers stop being magic and start being the obvious way to write leak-free, exception-safe C++.
Raw pointers still have a role: as non-owning access, for function parameters that borrow, for C interop. The thing that's gone away, in modern C++ practice, is raw pointers as owners. When you own something on the heap, use a smart pointer. The compiler will catch the rest.
What's next
Part 11, the finale of this series, is Undefined Behaviour. We've met UB in passing already: strict aliasing violations, writing through cast-away const, use-after-free. Part 11 pulls these together, explains why compilers are allowed to be so aggressive about UB, and walks through the debugging tools (sanitizers, valgrind, static analysers) that turn "mystery crash in release builds" into "caught at compile time." It's the post where everything we've learned about pointers earns its pay against the compiler's paranoia.
Test yourself
Seven questions covering RAII, the three smart pointers, move semantics, and common pitfalls. Five correct means you're ready for Part 11.
unique_ptr are true?std::move.B is false. That's
shared_ptr. unique_ptr holds just the pointer (plus the deleter if one is specified), with no refcount overhead.C is true. The destructor call is the only "extra" code, and it compiles to the same instructions as a manual
delete. Sizewise, unique_ptr with the default deleter is the same size as a raw pointer.D is false.
unique_ptr<T[]> is fully supported and calls delete[] correctly. std::make_unique<T[]>(n) is the idiomatic creator.E is true. Shorter, exception-safer, and avoids the
new/delete asymmetry that hides bugs.
Widget* and returns a raw Widget*:Widget* load_widget(const char* path); // returns owning pointer, caller must delete void inspect(Widget* w); // borrows, does not take ownership
load_widget's return in a smart pointer and pass it to inspect without transferring ownership. Write the code.unique_ptr, pass via .get() for non-owning access.
std::unique_ptr<Widget> w(load_widget("config.yml")); inspect(w.get()); // borrow without transferring ownership // w's destructor calls delete on the widget at scope exit. No leak.Why this works: the
unique_ptr constructor takes a raw pointer and assumes ownership, so w is now the sole owner. .get() returns the underlying raw pointer without releasing ownership; it's a view, not a handoff. inspect uses it and returns; w still owns the widget; the destructor cleans up.Common wrong answers: passing the
unique_ptr by value would move-transfer ownership into inspect, leaving w empty and the widget deleted at the end of inspect's parameter scope, except inspect doesn't take a unique_ptr, so it wouldn't even compile. Using w.release() would return the raw pointer and drop ownership, so when w dies it won't clean up, and the widget leaks.
struct Node { std::shared_ptr<Node> next; std::shared_ptr<Node> prev; }; auto a = std::make_shared<Node>(); auto b = std::make_shared<Node>(); a->next = b; b->prev = a; // local a and b go out of scope here
weak_ptr on the back-edge.
What happens: after both assignments, a's refcount is 2 (local + b->prev) and b's refcount is 2 (local + a->next). When the locals go out of scope, each refcount drops to 1, but neither hits zero because the two nodes keep each other alive through their mutual ownership. Nothing outside the cycle holds a reference, so the memory is orphaned and leaked.Fix:
struct Node { std::shared_ptr<Node> next; // forward: owning std::weak_ptr<Node> prev; // back: non-owning };Now
b->prev = a doesn't bump a's refcount, so when local a dies, a's count hits zero and cleanup runs, which drops a->next (the last strong ref to b), which drops b's count to 1, then to 0 when local b dies. To use prev, you call .lock(): if (auto p = b->prev.lock()) p->do_stuff();.The general pattern: if two
shared_ptrs point at each other, one of them must become a weak_ptr. Pick the direction that's "secondary" (the back-edge, the observer, the cache-lookup); keep the primary direction owning.
class Widget { public: std::shared_ptr<Widget> clone_handle() { return std::shared_ptr<Widget>(this); // hand out a shared_ptr to self } };
enable_shared_from_this.
What goes wrong: std::shared_ptr<Widget>(this) creates a brand-new control block with refcount 1, completely independent of whatever shared_ptr already manages this object. Now there are two control blocks pointing at the same Widget. Each will, when its refcount hits zero, call delete on the same pointer. The result is a double-free and undefined behaviour.Fix: inherit from
std::enable_shared_from_this and use shared_from_this():
class Widget : public std::enable_shared_from_this<Widget> { public: std::shared_ptr<Widget> clone_handle() { return shared_from_this(); // shares the existing control block } };The precondition:
shared_from_this() only works if the object is already owned by a shared_ptr. Calling it inside a constructor, or on an object allocated on the stack, will throw bad_weak_ptr. So the class has to be created via std::make_shared.This is a surprisingly common footgun. Any method that needs to hand out a
shared_ptr to this must use this pattern. Callback registration, async operations that capture the object for later, parent/child relationships that need a back-pointer, all of these need enable_shared_from_this in C++.
Select every pairing that's correct.
unique_ptr<Widget> documents this in the type and makes ownership transfer automatic (via move, since the return value is an rvalue).B: A cache with
shared_ptrs would leak memory forever (the cache would be the last holder of every entry). With weak_ptr, entries only live as long as something else holds a shared_ptr, and the cache gracefully forgets expired entries on lock().C: For borrowing access, a reference is better than any pointer. It's non-null by construction, non-owning by definition, and the compiler enforces it. Use
const Widget& for read, Widget& for read-write.D: Genuinely shared ownership across independent components is what
shared_ptr is for. (Watch out for cycles; if the graph has them, mix in weak_ptr for back-edges.)The pattern: start with "does this code own?" If no, raw pointer or reference. If yes and solo,
unique_ptr. If yes and shared, shared_ptr. If observing a shared resource without owning, weak_ptr.
auto a = std::make_unique<Widget>(); auto b = std::move(a); // line 2 a->do_work(); // line 3 b->do_work(); // line 4
a is null; dereferencing it is UB.
Line 2: std::move(a) doesn't actually move anything. It casts a to an rvalue reference. The move constructor of unique_ptr then transfers the underlying raw pointer from a to b, leaving a holding nullptr.Line 3:
a->do_work() dereferences a null pointer. Undefined behaviour, typically a segfault. The compiler won't warn you; in the type system, a is still a perfectly valid unique_ptr, just an empty one.Line 4:
b now owns the Widget and works normally.The rule: after
std::move(a), treat a as if its contents are gone. Don't dereference, don't rely on its value. The standard says a is "in a valid but unspecified state": the legal operations are destruction and assignment. Anything else depends on the type and is at best non-portable.This is one of the real costs of move semantics. The source object isn't destroyed, just emptied, and the compiler doesn't stop you from using it. The convention is "don't."
Scene owns its Meshes. A Renderer needs to access meshes to draw them but must not extend their lifetime. Occasionally a MeshInspector debug tool needs to observe a mesh and safely detect if it's been deleted between frames. Write the relevant declarations.class Scene { std::vector<std::shared_ptr<Mesh>> meshes; // owns, allows sharing public: const std::vector<std::shared_ptr<Mesh>>& get_meshes() const { return meshes; } }; class Renderer { public: void draw_frame(const Scene& scene) { for (const auto& m : scene.get_meshes()) { draw(*m); // use without extending lifetime } } }; class MeshInspector { std::weak_ptr<Mesh> watched; // observer, survives deletion public: void watch(std::shared_ptr<Mesh> m) { watched = m; } void debug_dump() { if (auto m = watched.lock()) { std::cout << "mesh is alive: " << m->name() << "\n"; } else { std::cout << "mesh was destroyed\n"; } } };Scene owns the meshes via
shared_ptr, not unique_ptr, because the design calls for meshes that can be referenced elsewhere (the inspector). If no sharing were needed, unique_ptr would be cleaner.Renderer takes the scene by
const Scene& and iterates meshes via a const reference. It doesn't need to touch lifetime at all; this is exactly the "raw access without ownership" case that references are best at.MeshInspector holds a
weak_ptr, which lets the mesh die naturally (when the Scene drops it) while the inspector still exists. The lock()-returns-maybe pattern is the textbook use of weak_ptr.What you should not do: make
Renderer hold its own shared_ptrs (would unnecessarily keep meshes alive across frames); make MeshInspector hold a raw pointer (no way to detect if the mesh was freed since last check); make Scene's meshes unique_ptrs (forbids the sharing the design requires).
Comments