5 ms·
This is not untrue, but the counterpoint would be that the intersection of a) the number of situations where the shared pointer copy would have any measurable i
by w0utert 7y ago
This is not untrue, but the counterpoint would be that the intersection of a) the number of situations where the shared pointer copy would have any measurable influence on performance, and b) using shared pointers is a good idea for that particular problem in the first place, is likely vanishingly small.
The main performance ‘problem’ with shared pointers is locking and contention in heavily multithreaded scenarios, which is why pretty much nobody uses a reference counting memory model for these kinds of things. In the vast majority of single or lightly threaded scenarios I think you will be hard pressed to find any reasonable algorithm implemented using shared pointers where the reference counting overhead is even measurable. So it’s not a strange thing at all to deprioritize hypothetical performance gains over clear intent of your code. Best thing is to stay away from shared pointers unless you really need them anyway, as they promote designs with sloppy ownership semantics. Been there, done that, try to get rid of them years later... :-/
The almost obligatory Herb Sutter reference made me chuckle by the way. He says so many things about what he thinks is the right thing to do in C++, and gets quoted almost as an absolute authority on good C++ practices, but quite often (not always, mind you) I find his arguments weak and unconvincing, and his recommendations sometimes downright counterproductive.
- linkdd 7y ago> Best thing is to stay away from shared pointers unless you really need them anyway, as they promote designs with sloppy ownership semantics. Could you elaborate? I'm not an expert in C++ but smart pointers did simplify a lot of my code and I never met the case where it was a problem. Like everything else in C++ (and any low-level language), they should be used carefully.
- mytherin 7y agoIn my experience unique pointers are usually a better fit than shared pointers. That is not to say shared pointers do not have their place, but they can definitely be abused. Especially for developers that come from garbage collected languages, it might be tempting to replace all pointers with shared pointers and call it a day. Not only can this cause performance issues when these are used in performance critical locations, but you can also lose track of who owns what very easily. It is also very easy to accidentally create cyclic references with shared pointers. Suppose you have a class like this: struct Node { shared_ptr<Node> parent; shared_ptr<Node> child; }; This rather innocent looking class creates a cycle when you fill in the values, because the parent and child reference each other. That cycle now means your nodes will not be cleaned up anymore. Unique pointers do not have any of these problems. They cannot create cycles, are very cheap and they make it very explicit who owns what. They are not suitable for every situation (i.e. when there is actually shared ownership shared pointers should be used) but in most situations I find that they are sufficient and have a lot less pitfalls than shared pointers.
- im3w1l 7y ago> They cannot create cycles, I don't think this is true. Couldn't you accidentally make two objects "own" each other?
- deleted 7y ago[deleted]
- mytherin 7y agoYes, excellent point. It is possible to make cycles, it is just a lot more difficult to do accidentally than with shared pointers.
- saagarjha 7y ago> This rather innocent looking class creates a cycle when you fill in the values, because the parent and child reference each other. That cycle now means your nodes will not be cleaned up anymore. It’s fairly common knowledge, but just to answer the inevitable question: you can avoid the cycle with weak pointers.
- w0utert 7y agoWith 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.