Question

Suppose I had a Manager Class that held a vector of some object:

class SomeObjectManager
{
private:
    std::vector<SomeObject> _heldObjects;
};

And in that class I had some function that iterated through said vector to return the requested object.

SomeObject getSomeObjectByName(std::string nameToFind);

What I need to know is when is it proper to use smart pointers. Should I actually be returning something like below?

std::shared_ptr<SomeObject> getSomeObjectByName(std::string nameToFind);

Or should I be using something else like unique_ptr or weak_ptr? I want the SomeObjectManager class to own the actual object being returned and never have said SomeObject be deleted unless the Manager makes it so.

I have only recently came back to the C++ world after being in C# mode for quite some time; thanks for the help and clearing up my confusion.

I have read a lot about this matter but never really found a straight answer to my particular situation.


Edit #1

I'd like to reword my last few sentences with this:

I want the SomeObjectManager class to own the actual object being returned and never have said SomeObject be removed from the vector and subsquently deleted, fall out of scope, until the Manager forces it to do so. For example:

void SomeObjectManager::removeSomeObjectByName(const std::string& objectToRemove);

This would just iterate over the vector, finding said SomeObject, and remove it from the Vector.

Was it helpful?

Solution

Since SomeObjectManager is the owner of the SomeObject instances (stored in its std::vector data member), I'd just return raw pointers, since they are actually observing pointers.

std::vector<SomeObject> _heldObjects;

SomeObject* getSomeObjectByName(const std::string& nameToFind) {
    ... find index of object corresponding to 'nameToFind'
    return &_heldObjects[foundIndex];
}

(Note that I passed nameToFind using reference to const, since I assume that nameToFind is an input string, so if inside the method you are just observing that string, you can avoid deep-copies using const &).

You must pay attention when you have owning raw pointers (they should be wrapped inside safe RAII boundaries), but observing raw pointers are fine.

Just make sure that the lifetime of SomeObjectManager exceeds that of its clients, to make sure that the clients are referencing valid objects.

Note also that if you add new items to the vector data member (e.g. using std::vector::push_back()), the addresses of the previous SomeObject instances stored in the vector can change. So, if you gave pointers to those outside, they become invalid.

So, make sure that the vector size and vector content are not changed before you give pointers to its elements to client code outside.

An alternative would be to have std::vector<std::unique_ptr<SomeObject>> as data member. In this case, even if the vector is resized, the addresses you returned using the smart pointers (in particular using std::unique_ptr::get()) are still valid:

std::vector<std::unique_ptr<SomeObject>> _heldObjects;

SomeObject* getSomeObjectByName(const std::string& nameToFind) {
    ... find index of object corresponding to 'nameToFind'
    return _heldObjects[foundIndex].get();
}

PS
Another option might be returning references to const SomeObject (assuming that this use of const makes sense in your design):

std::vector<SomeObject> _heldObjects;

const SomeObject& getSomeObjectByName(const std::string& nameToFind) const {
    ... find index of object corresponding to 'nameToFind'
    return _heldObjects[foundIndex];
}

OTHER TIPS

If your program runs in a single thread, you are mostly good with returning raw pointers or references to the objects that are stored in the vector, if you have sufficient discipline.

Since the manager privately owns the vector and the objects inside, and thus controls when objects are deleted, you can make sure that there remain no invalid pointers to objects that have been deleted (this isn't automatically guaranteed!).
Basically, the manager must only ever delete objects when it knows that nobody holds a reference to that object, for example by only doing this at distinct, well-defined times (such as at program end, or when it knows that no consumers remain, or such).
Reference counting is one way of doing that, and it is what shared_ptr does internally, too (well, no... strictly to the letter of the specification, reference counting isn't required, only the visible behavior is defined, but practially all implementations do it).

The process of "deleting" an object would thus merely decrement the reference counter (much like in a managed language) and the object would really cease to exist when the reference counter reaches zero. Which is probably but not necessarily immediately the case when you "delete" it. It might as well take a few moments before the object is actually destroyed.
That approach works "automatically" without a lot of diligence and rigid discipline, and it can be implemented simply by storing shared_ptrs of your objects in the vector and returning either shared_ptrs or weak_ptrs.

Wait a moment! Then why are there even weak_ptrs if you can just return a shared_ptr too? They do different things, both logically and practically. shared_ptrs own (at least partially), and weak_ptrs do not. Also, weak_ptrs are cheaper to copy.

In a multi-threaded program, storing shared_ptrs and returning a weak_ptr is the only safe approach. The weak_ptr does not own the resource and thus cannot prevent the manager from deleting the object, but it gives the holder a reliable and unambiguous way of knowing whether the resource is valid and that the resource will remain valid while you use it.

You return that weak_ptr, and when the consumer actually wants to use the object, it converts the weak_ptr to a temporary shared_ptr. This will either fail (giving a null pointer) so the consumer knows that the object has been deleted, and it may not use it. Or, it will succeed, and now the consumer has a valid pointer with shared ownership of an object which is now guaranteed to remain valid while it is being used.

There is nothing in between "valid" and "invalid", no guessing, and nothing that can fail. If you successfully converted to a valid temporary shared_ptr, you are good to go. Otherwise, the object is gone, but you know that.
This is a big, big plus in terms of safety. Even if the manager "deletes" the object while you are using it, your program will not crash or produce garbage, the object remains valid until you stop using it!

Arguably, this somewhat blurs the "the manager deletes objects when it chooses to do so" paradigm, but it really is the only way of doing it safely. The manager is still the one in control of what objects to delete, it only cannot delete an object immediately while it is in use (which would possibly result in a terrible desaster). It can, however, at any time schedule the deletion for the next possible time by removing its shared_ptr and thus decrementing the reference count.

The only obvious showstopper is the case where an object must be destroyed immediately (because the destructor has side effects that must happen immediately without delay). But in this case, it is very hard (a nightmare!) to get concurrent access right. Luckily, that's a very rare scenario, too.

Return a reference (or regular pointer) to the SomeObject from your function. The reference is valid so long as it remains in the vector, and the vector is not reallocated (careful with that, maybe use a list instead or vector of unique_ptr). When removed from the vector, the object is dead and all references to it are no longer valid. (Again careful removing element in the middle)

If you are not storing your objects as std::shared_ptrs, then it wouldn't make any sense to return an std::shared_ptr. Not even sure how you are going to do it. I don't think there is a way to wrap an already existing pointer within a smart pointer. If you already have the data there, you can just return a regular const pointer to it. That way you will avoid the overhead that it takes to copy the object contents.

You have a choice of using shared_ptr or weak_ptr in this case. Which you use will depend on the lifetime you want for the object.

If you only want the object to be valid whilst the SomeObjectManager has a reference to it and a client is using it at that time then use weak_ptr. If you want a reference to remain valid if either the SomeObjectManager has a reference and a client stores a reference to it.

Here's an example with a weak_ptr.

std::weak_ptr<SomeObject> weakref = getSomeObject();   
// weakref will not keep the object alive if it is removed from the object manager.

auto strongref = weakref.lock();
if ( strongref ) {
     // strongref is a shared_ptr and will keep the object alive until it 
     // goes out of scope.
}

This can be useful in a multi-threaded environment as the shared_ptr reference count access is thread-safe. However, it does mean that a client can extend the lifetime of an object longer than you may like.

If you want to use smart shared pointers, the vector itself should use the smart pointer.

class SomeObjectManager
{
private:
    std::vector<std::shared_ptr<SomeObject> > _heldObjects;
};

But then you're safe.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top