Unlike most languages that have ref-counting build in, shared_ptr also doesn't provide anything to deal with cyclic dependencies, so you can end up with memory leaks.
The most important reason however is simply that you don't need it like 99% of the time, unique_ptr provides enough functionality to work just fine as a shared_ptr replacement in most situations. And in the rare cases where you really need a shared_ptr, you can just convert a unique_ptr into one.
They are quite heavily (and badly) used in some code bases (ROS). I'm planning a future article that covers shared_ptr in more details.
The surprising thing about shared pointer is that a `const shared_ptr<T>` means that you can modify the contents of T. This makes the problem, mentioned in the parent, of keeping track of who can modify the object where impossible. I've never encountered a `const shared_ptr<const T>` but that would be a better approach.
Languages with GC-managed runtimes (Java, C#, Go, Swift, et. al.) are actually significantly more performant for almost all heap-bound use cases, actually. Reference counting kinda sucks for typical code.
When a GC-managed language uses reference counting as implementation algorith, the compiler might be able to optimize the reference counting to only occur when it is unavoidable or too costly to reason about, in a similar vein to bounds checking.
When using library types for reference counting, there is no way for the compiler to implement such optimizations, unless the types are somehow tagged with compiler intrisics so that they could apply the same kind of optimizations.
Aren’t refcounts in shared_ptr done as part of the copy constructor and destructor? It seems like vanilla copy elision would count as optimizing away the refcounts.
All of them have to do counter booking, value count and accessors, also possibly handle weak_ptr booking.
Plus how it actually works isn't part of ISO C++, so you're betting your luck on how a specific C++ is actually going to implement it, and possible issues when compiler versions change, same compiler or to another one.
Swift does not have a GC managed runtime in the same sense as the other languages you listed [0], it’s basically a bunch of refcounts inserted by the compiler, with similar performance characteristics to Arc in Rust or shared_ptr in C++. (For classes, at least. Structs are value types and stack-allocated.)
[0] Yes, automatic refcounting is a form of garbage collection, but Java/C#/Go use tracing GC’s and not direct refcounting, whereas with Swift it’s more like the compiler is wrapping all objects in a shared_ptr for you, and so destruction is explicit and happens at exact points.
Although unique ptr is zero cost after make_unique(), I avoid polluting my heap unnecessarily. I've never benchmarked this though (keeping various objects on stack vs heap as unique ptr and how that impacts memory accesses)
I'm quite junior. Appreciate anyone pointing out if anything I said doesn't make sense.
Sounds about right. Shared ownership is fairly rare though, and you often only need shared access (reference/pointer if nullable) and can provide other, more explicit, ways of managing the lifetime.
> unique ptr is zero cost after make_unique()
Kind of, but compared to the stack, it could cause caching inefficiency because your heap-allocated thing could be almost anywhere, but your stack-allocated thing is probably in the cache already.
Also if you lean in too much on shared/unique pointers for large amounts of tiny objects you'll most likely end up in a situation where Java-style garbage collection would be more efficient.
E.g. manual or semi-manual memory management is mostly about controlling the overall memory layout of your application's data to improve throughput, reducing the number of individual heap allocations is just a useful side effect.
So instead of just accessing memory, you have the cost of cross-cache coherency operations between CPU cores.
- Wrapping all objects in shared pointer is annoying. - If you stick to that convention, you have to do it on every call side, while you only have to implement RAII once. - You can enforce invariants of your class with RAII, that you can't with a plain shared_ptr - Regarding efficiency: It has the overhead of reference counting plus you have to store all objects on the heap instead of the stack. In hot loops this may hurt cache locality.