Search code examples
c++singletonshared-ptr

Removing an object from a singleton-managed container upon destruction


I have a Singleton class that manages a container of Items, exposing public functions that allow Items to be added or removed from the container.

class Item;
typedef std::shared_ptr<Item> ItemPtr;

class Singleton
{
public:
  static Singleton& Instance()
  {
    static std::unique_ptr<Singleton> Instance(new Singleton);
    return *Instance;
  }

  void Add(ItemPtr item)
  {
    mContainer.push_back(item);
  }

  void Remove(ItemPtr item)
  {
    for (auto it = mContainer.begin(); it != mContainer.end(); it++)
      if (*it == item)
        mContainer.erase(it);
  }

private:
  std::vector<ItemPtr> mContainer;
};

I'd like Item to have the ability to add itself to the Singleton container via an Add() method, and remove itself from the container upon its destruction.

class Item
{
public:
  Item() {}

  ~Item() 
  {
    Singleton::Instance().Remove(ItemPtr(this));
  }

  void Add()
  {
    Singleton::Instance().Add(ItemPtr(this));
  }
};

When I run the example below, I get a crash on Singleton::Remove(), specifically a EXC_BAD_ACCESS on mContainer.begin().

int main()
{
  Item* a = new Item();
  Item* b = new Item();

  a->Add();
  b->Add();

  delete a;
  delete b;
}

This seems to indicate that mContainer no longer exists. Looking at the call stack, I can also see one of the root call stack frames is the destructor Singleton::~Singleton(), which would explain why mContainer is no longer there.

I've tried a different approach : instead of using std::shared_ptr<Item> I simply used raw pointers (i.e., Item*) with the appropriate substitutions in the code. It worked without problems.

My questions are:

  • I guess what's happening is that the ownership of the Item objects is only released by the shared_ptr after the destruction of Singleton, which causes the error. Is this correct?
  • Is it impossible to do what I want to do if the container in Singleton is of shared_ptr<Item>?
  • If not, how could I do it?

Solution

  • The wisdom of doing this in the first place notwithstanding, what you want can be achieved if you're willing to use, and abide by the restrictions of, std::enabled_shared_from_this. See below:

    #include <iostream>
    #include <algorithm>
    #include <memory>
    #include <vector>
    
    struct Item;
    typedef std::shared_ptr<Item> ItemPtr;
    
    class Singleton
    {
    private:
        Singleton() {}
    
    public:
        static Singleton &Instance()
        {
            static Singleton s;
            return s;
        }
    
        void Add(ItemPtr item)
        {
            mContainer.emplace_back(std::move(item));
        }
    
        void Remove(const ItemPtr& item)
        {
            mContainer.erase(
                std::remove(mContainer.begin(), mContainer.end(), item), 
                mContainer.end());
        }
    
        void Clear()
        {
            mContainer.clear();
        }
    
    private:
        std::vector<ItemPtr> mContainer;
    };
    
    // note derivation. this means you can get a std::shared_ptr<Item>
    // via `shared_from_this` , but it also means the object itself
    // MUST be an actual shared object to begin with.
    struct Item : public std::enable_shared_from_this<Item>
    {
        void Add()
        {
            Singleton::Instance().Add(shared_from_this());
        }
    };
    
    
    int main()
    {
        ItemPtr a = std::make_shared<Item>();
        ItemPtr b = std::make_shared<Item>();
    
        // add to the singleton container
        a->Add();
        b->Add();
    
        // report reference count of 'a'
        std::cout << "before removal 'a' has " << a.use_count() << " references\n";
        Singleton::Instance().Remove(a);
        std::cout << "after removal 'a' has " << a.use_count() << " references\n";
    }
    

    Output

    before removal 'a' has 2 references
    after removal 'a' has 1 references
    

    The most important part of this is the creation of a and b in main . Notice they are, in fact, managed by std::shared_ptr enshrouding from inception. This is required for std::enable_shared_from_this to work correctly. The rest is fairly straight forward. The ability to get a reference-bumped std::shared_ptr from within the body of any member of Item is done via the shared_from_this() member provided from the base class std::enable_shared_from_this.

    In short, taking this approach will work for you, but at no point can you use shared_from_this() unless the object it is being fired upon is already managed by a std::shared_ptr in the first place. Keep that in mind.