3 ms·
With shared pointers it is very hard to reason at which point things get deleted, from just looking at the code. This can become particularly messy when you com
by w0utert 7y ago
With shared pointers it is very hard to reason at which point things get deleted, from just looking at the code. This can become particularly messy when you communicate them freely across API boundaries, because that allows callers to just store the shared pointers they get from your API instead of e.g. making private copies or not keeping references at all. Especially in large code bases worked on by multiple people, object lifetime will grow beyond anyone’s comprehension, and people writing new code on top of the existing code will have little other choice than just default to copying around the shared pointers everywhere.
If you don’t know what the lifetime of your objects is, it is very difficult to follow RAII best practices, to control memory use (a shared pointer that is held on to where it is not necessary is basically a leak), to avoid high and/or unpredictable allocation/deallocation cost (compared to e.g. arena allocators or just stack copies), etc. On top of that there is a very conceivable risk of introducing cycles (and hence leaks) when overusing shared pointers. And last but not least debugging code that defaults to shared pointers everywhere can be a nightmare.
These are all practical downsides, but to me personally the argument that you have to be explicit about your memory model (allocation, ownership, lifetime) and use the right tools for your design is even more important. Shared pointers are definitely a very useful tool, but only in situations where you truly want to have shared ownership, and not just because they are convenient because they save you from having to think about these things. Over time I’ve found that more often than not, using strictly unique_ptr for ownership, raw pointers and references to avoid copies, and plain value copies where appropriate and not performance critical, leads to code that is much easier to reason about, has less bugs, etc.
- magicalhippo 7y agoIndeed. Practical example from another language. I was using a thread pool written by someone else to execute some tasks. For reasons, the tasks were essentially represented as shared_ptr's to an object instance. The pool dequeued a task, issued it to a thread, the thread then called a method on the object and asked for more work. I stumbled over a case where the destructor of the task was not always called, leaving all sorts of handles hanging. Turned out the code that dequeued a task did so into a local variable, and didn't clear that variable after it had passed it on to the thread. So when the pool ran out of work, a reference to the last task was being held by the local variable in the pool code, and that reference was only released when new work arrived or the pool was destroyed.