Search code examples
c++shared-ptrunordered-map

How to create a map of smart pointers that deletes its elements when the pointers go out of scope


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?


Solution

  • 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;
    };