Sometimes some object A needs to interact with another object B, e.g., A calls one of B’s methods. In a memory-unsafe language like C++, it is left to the programmer to assure that B outlives A; if B happens to be already destructed, this would be a use-after-free bug. Managing object lifetimes can be tricky, especially with asynchronous code. Perhaps unneeded, but here is a simple example of the problem: struct Foo { void foo (); }; struct Bar { Foo * f ; void call_foo () { f -> foo (); } }; int main (){ Bar A ; { Foo B ; A . f = & B ; } // B destructed A . call_foo (); // Oops return 0 ; } You could of course argue that one should use a more modern, memory-safe language such as Rust to rule out such bugs by construction. But sunken cost in an existing codebase, or the more mature library ecosystem of an older language like C++ can easily render such a rewrite economically infeasible. By the way, (and please correct me in case my understanding here is incorrect as a very inexperienced Rust programmer), getting Rust’s borrow checker to understand and agree with object lifetimes in your asynchronous code without having to sprinkle 'static in many places can vary from challenging to currently impossible. Also, C++ compilers are improving, so by enabling all warnings and by using features like Clang’s lifetimebound attribute you will already catch trivial instances like the one above, but there is currently no guarantee that more opaque instances of the same bug pattern, possibly spread over multiple translation units, will be caught by the compiler. One way to avoid lifetime problems like the one sketched above in C++ (at least C++11, that is) is for A to manage the lifetime of B, say via a std::unique_ptr or std::shared_ptr . struct BarOwnsFoo { std :: unique_ptr < Foo > f ; void call_foo () { f -> foo (); } }; int main (){ BarOwnsFoo A ; { auto B_ptr = std :: make_unique < Foo > (); A . f = std :: move ( B_ptr ); } // B kept alive inside A A . call_foo (); // Okay return 0 ; } Safe interactions between separately lifetime-managed objects Sometimes, however, it may be undesirable to have an ownership relation as in the above code example. Especially in case of shared_ptr , reasoning about when some object (which might be holding various expensive resources like memory) actually gets destructed in your program becomes harder. So, instead of always coupling lifetime to ownership, we would like to have a pointer-like primitive that enables safe interactions between separately lifetime-managed objects. By safe, we mean triggering some well-defined action like an assertion failure or throwing a std::logic_error in case the pointee has already been destructed, instead of triggering Undefined Behavior (UB). Move semantics and pointer invalidation Another problem, which we have not discussed yet, is the scenario in which we take a pointer or reference to some object, and subsequently, we move that object. The move operation invalidates the pointer or reference; dereferencing the pointer (or using the reference) after the move operation (but before the moved-from object is destructed) would be a use-after-move bug. Besides, it would be a logical bug, in that we would still like to interact with the moved object, not with the ‘empty’ moved-from object. int main (){ Bar A ; Foo B ; A . f = & B ; Foo C = std :: move ( B ); A . call_foo (); // Oops again // ... } Instead of having to remember that we may not move an object after having taken a pointer or reference, or forbid moving the object by make the object type non-movable, we would like our pointer-like primitive to be automatically updated. Proposed primitive: safe_ptr We propose a design for a safe pointer to an object of type T that is weak in that it does not have ownership semantics, and gets notified in case the pointee is either destructed or moved. We will pay a price at runtime for these extra guarantees in terms of a small heap-allocated state, a double pointer indirection when accessing the pointee (comparable to a virtual function call), and a check against nullptr . Note that our use case is in a single-threaded context. Hence, the word safe should not be interpreted as ‘thread-safe.’ Single-threadedness greatly simplifies the design; we need not reason about race conditions such as one where an object is simultaneously moved and accessed on different threads. Extending the design to a thread-safe one is left as an exercise to the reader. The main idea of our design is to internally use a std::shared_ptr to share ownership of a (heap-allocated) pointer T* . The ownership is shared by the pointee and all safe_ptr instances. The pointee type T must derive from the base class safe_ptr_factory (templated on T , using the Curiously Recurring Template Pattern) with a destructor that automatically notifies the shared state about destruction by setting T* to nullptr , and with a move constructor and move assignment operator that update T* to the new this pointer (which we must properly cast with static_cast ). The double indirection thus comes from having to dereference the shared_ptr to obtain an ordinary pointer T* , after which you must dereference once more to access the pointee. You might recognize an instance of Gang-of-Four’s observer pattern in our design. Below you find an example of how safe_ptr protects against UB in case of use-after-free: class Baz : public safe_ptr_factory < Baz > {}; safe_ptr < Baz > sp ; { Baz b ; sp = b . safe_ptr_from_this (); } // b destructs here but sp is notified * sp ; // throws std::logic_error exception and how safe_ptr remains valid after a moving the pointee: class Fubar : public safe_ptr_factory < Fubar > { public: void foo (); }; safe_ptr < Fubar > sp ; Fubar b ; sp = b . safe_ptr_from_this (); Fubar c = std :: move ( b ); // b is moved but sp is notified sp -> foo (); // Okay! Full implementation Below, we give the full implementation, which also specifies the behavior in case the pointee type is copied. You can also interact with the code in Matt Godbolt’s Compiler Explorer.