As has been said, the pendant of std::shared_ptr is std::weak_ptr which allows to hold onto the resource without actually holding onto it. Therefore a trivial transformation would be to store a weak_ptr<T> as the value in your map to avoid artificially maintaining the object alive...
However there is an issue with this scheme: a space leak. The number of keys in your map will never decrease, meaning that if you load 1000 "resources" and release 999 of them, your map still has 1000 keys, 999 of them associated to a useless value!
The trick is, however, rather simple: upon destruction, the registered object should notify those which have a reference to it! This does impose a number of limitations though:
- the name of the object should never change once it has been registered
- an object should never been registered more than once and/or maintain a list of all those it is registered with
Finally, there is also the issue that those the object was registered to could die before the object does... Getting kinda complicated isn't it ?
So, here is our plan of attack:
- What is only ever run once per instance ? The constructor.
- How do you ensure liveness "passively" ? Using
std::weak_ptr
Let's go!
// Object.hpp class Cache; class Object: public enable_shared_from_this<Object> { public: // std::shared_ptr<Object> shared_from_this(); -- inherited Object(std::string const& name, std::shared_ptr<Cache> const& cache); virtual ~Object(); Object(Object const&) = delete; Object& operator=(Object const&) = delete; std::string const& name() const { return _name; } private: std::string _name; std::weak_ptr<Cache> _cache; }; // class Object // Object.cpp #include <Object.hpp> #include <Cache.hpp> Object::Object(std::string const& name, std::shared_ptr<Cache> const& cache): _name(name), _cache(cache) { if (cache) { cache->add(this->shared_from_this()); } } Object::~Object() { std::shared_ptr<Cache> c = _cache.lock(); if (c) { c->release(*this); } }
A couple of weird things going on here:
- by passing the name in the constructor we guarantee that it's set, and since we don't provide any setter it cannot be modified either (unless
const_cast...) - inheriting from
enable_shared_from_this means that if the object's lifetime is managed by a shared_ptr then using shared_from_this we can get a shared_ptr pointer to it - we have to be careful that we do have a reference to a still alive cache in the destructor, so we check for it.
- let's use a
virtual destructor, just to be on the same side.
Okay, so let's go on:
// Cache.hpp #include <Object.hpp> class Cache: public std::enable_shared_from_this<Cache> { friend class Object; public: // std::shared_ptr<Cache> shared_from_this(); -- inherited std::shared_ptr<Object> get(std::string const& name) const; void release(Object const& o); private: typedef std::weak_ptr<Object> WeakPtr; typedef std::map<std::string, WeakPtr> Map; void add(std::shared_ptr<Object> const& p); Map _map; }; // class Cache // Cache.cpp #include <Cache.hpp> std::shared_ptr<Object> Cache::get(std::string const& name) const { auto const it = _map.find(name); if (it == _map.end()) { return std::shared_ptr<Object>(); } return it->second.lock(); } void Cache::release(Object const& o) { _map.erase(o.name()); } void Cache::add(std::shared_ptr<Object> const& p) { assert(p && "Uh ? Should only be accessed by Object's constuctor!"); _map[p->name()] = p; // Note: override previous resource of same name, if any }
This seems pretty easy now. Usage:
int main() { std::shared_ptr<Cache> c{new Cache{}}; // cannot be stack allocated no longer { std::shared_ptr<Object> o{new Object{"foo", c}}; assert(c->get("foo")); } assert(c->get("foo") == nullptr); std::shared_ptr<Object> o{new Object{"foo", c}}; c.reset(); // destroy cache // no crash here, we just do not "unregister" the object }