I want to a map container that takes a smart pointer to a custom type. When you put the key in, you get shared_ptrs
to the object, but when all of those shared_ptrs
go out of scope, the object is destroyed inside the map (in contrast to a normal map, where the object persists until the map goes out of scope or until it is explicitly deleted).
Here's what I want to achieve:
SpecialMap<int, std::string> map;
{
std::shared_ptr<std::string> item = map.getElement(3);
} // element at 3 is deleted *inside the map* here, because the pointer goes out of scope.
assert(map.getSize() == 0);
I think that this can be done by giving the map a member type of std::weak_ptr
, which doesn't increment the reference count, and then using a std::shared_ptr
with a custom deleter which removes the element from the map, rather than calling delete
. But I'm struggling with how to insert items to the map. Here's what I'm trying right now.
(In this example I'm just using int
and string
to keep things simple--I'll template it properly later on).
class SpecialMap
{
public:
std::shared_ptr<std::string> getElement(int i)
{
auto result = map.try_emplace(i, std::shared_ptr<std::string>(new std::string("text"),
[this](std::string* s) // here is the custom deleter
{
for (auto it = map.begin(); it != map.end();)
{
if (&*it->second.lock() == s)
{
map.erase(it);
return;
}
else ++it;
}
}));
return result.first->second.lock();
}
int getSize() const
{ return map.size(); }
private:
std::unordered_map<int, std::weak_ptr<std::string>> map;
};
This code doesn't work because the shared_ptr
inside try_emplace
goes out of scope immediately, deleting the item before it can be returned.
Can anyone suggest a better way of interacting with unordered_map
here? Or is there a better way to approach the problem?
Consider the following part of the deleter...
for (auto it = map.begin(); it != map.end();) {
if (&*it->second.lock() == s) {
map.erase(it);
return;
}
else
++it;
}
But the deleter is only being called because the ref count has become zero. That being the case the call...
it->second.lock()
will return a shared pointer that refers to a null pointer. Hence the equality will never hold and the corresponding element in the unordered_map will never be erased.
The usual way to deal with something like this would be to have something like...
using container_type = std::unordered_map<int, std::weak_ptr<foo>>;
container_type m_map;
std::map<foo *, container_type::const_iterator> m_iters;
Here m_map
is the real data store and m_iters
can be used to find the appropriate iterator to erase when the shared pointers ref count becomes zero and the deleter is called.
class special_map {
using container_type = std::unordered_map<int, std::weak_ptr<std::string>>;
public:
using const_iterator = container_type::const_iterator;
std::shared_ptr<std::string> get_element (int i)
{
/*
* Check to see if the requested element already exists. If so simply
* return it.
*/
auto iter = m_map.find(i);
if (iter != m_map.end())
return iter->second.lock();
std::shared_ptr<std::string> value(
new std::string,
[this](std::string *s)
{
std::cout << "deleter called on " << s << "\n";
if (auto i = m_iters.find(s); i != m_iters.end()) {
m_map.erase(i->second);
m_iters.erase(i);
}
delete s;
});
iter = m_map.emplace(i, value).first;
auto sp = iter->second.lock();
m_iters[sp.get()] = iter;
return sp;
}
int get_size() const
{
return m_map.size();
}
const_iterator begin () const
{
return m_map.begin();
}
const_iterator end () const
{
return m_map.end();
}
private:
container_type m_map;
std::map<std::string *, container_type::const_iterator> m_iters;
};
std::ostream &operator<< (std::ostream &os, const special_map &v)
{
std::cout << "special_map@" << &v << "(" << v.get_size() << " elements)...\n";
for (const auto &i: v) {
auto sp = i.second.lock();
std::cout << "[" << i.first << "] --> [std::string@" << sp.get() << "/" << (sp.use_count() - 1) << "]\n";
}
return os << "\n";
}
int main ()
{
{
special_map sm;
{
auto s1 = sm.get_element(5);
std::cout << sm;
{
auto s2 = sm.get_element(5);
std::cout << sm;
}
std::cout << sm;
}
std::cout << sm;
}
}
Example output from this is...
special_map@0x7ffedf44d6a0(1 elements)...
[5] --> [std::string@0xf75bc0/1]
special_map@0x7ffedf44d6a0(1 elements)...
[5] --> [std::string@0xf75bc0/2]
special_map@0x7ffedf44d6a0(1 elements)...
[5] --> [std::string@0xf75bc0/1]
deleter called on 0xf75bc0
special_map@0x7ffedf44d6a0(0 elements)...
Edit 1
If the key type being used is sufficiently 'lightweight' as with int
in the current case then it might be possible to avoid the use of the extra m_iters
container by having the deleter lambda capture the key by value and performing the map lookup based on that. In that case the code becomes...
class special_map {
using container_type = std::unordered_map<int, std::weak_ptr<std::string>>;
public:
using const_iterator = container_type::const_iterator;
std::shared_ptr<std::string> get_element (int i)
{
/*
* Check to see if the requested element already exists. If so simply
* return it.
*/
auto iter = m_map.find(i);
if (iter != m_map.end())
return iter->second.lock();
std::shared_ptr<std::string> value(
new std::string,
[this, i](std::string *s)
{
std::cout << "deleter called on " << s << "\n";
m_map.erase(i);
delete s;
}
);
return m_map.emplace(i, value).first->second.lock();
}
int get_size() const
{
return m_map.size();
}
const_iterator begin () const
{
return m_map.begin();
}
const_iterator end () const
{
return m_map.end();
}
private:
container_type m_map;
};