I started tracking object destruction using boost::signals2. I wrote a small test just to see if I could still use signals in destructors here. It seemed to work. I then started using it for tracking lists of objects which reference other ones. My structure is more or less as so:
For an abbreviated diagram of the structure: http://oi50.tinypic.com/16c8cwn.jpg
I have two classes IModel and IBlock. The IModel has many IBlocks and an IBlock has a IModel parent. However, there is a special IBlock called an IModelBlock. This block has a referenced IModel in addition to its parent. This is something of a "connector" between IModels. I wanted IModels to know which IModels were using them so I implemented a reference counting system which used the signals which happen during destruction of IModel and IBlock to track which models were using another model.
I have my IBlock pure virtual class (except for the destructor obviously):
class IBlock
{
public:
virtual ~IBlock() { this->sigBlockDestroying(this); }
...
boost::signals2::signal<void (IBlock*)> sigBlockDestroying; //raised when the destructor is called
};
My IModelBlock header (pure virtual class):
class IModelBlock : public IBlock
{
public:
virtual ~IModelBlock() {}
...
};
My IModel header (pure virtual class):
class IModel
{
public:
...
virtual void nowUsedBy(IModelBlock* block) = 0;
};
My implementation of the IModelBlock constructor (ModelBlock class) which informs a model that it is being used:
ModelBlock::ModelBlock(IModel *parent, long id, boost::shared_ptr<IModel> model)
{
//...lots of stuff happens involving the parent and model...then:
//tell the model we see them
model->nowUsedBy(this);
}
Here's where it gets hairy
My implementation of IModel (Model) defines the following:
class Model : public IModel
{
public:
...
virtual ~Model();
...
protected:
...
void onModelDestroying(IModel* model);
void onModelBlockDestroying(IBlock* modelBlock);
...
//counts for this model being used
std::map<IModel*, int> useCounts;
std::map<IModel*, boost::signals2::connection> modelDestructionConnections;
std::vector<boost::signals2::connection> modelBlockDestructionConnections;
};
Model::~Model()
{
typedef std::pair<IModel*, boost::signals2::connection> ModelDestructionRecord;
BOOST_FOREACH(ModelDestructionRecord record, this->modelDestructionConnections)
{
record.second.disconnect();
}
BOOST_FOREACH(boost::signals2::connection& connection, this->modelBlockDestructionConnections)
{
connection.disconnect();
}
}
void Model::nowUsedBy(IModelBlock *block)
{
if (block->isOrphan())
return; //if the block is an orphan, there isn't actually a model using it
if (useCounts.count(block->getParentModel()))
{
//increment this use count
useCounts[block->getParentModel()]++;
}
else
{
useCounts[block->getParentModel()] = 1;
//subscribe to the model's destruction
modelDestructionConnections[block->getParentModel()] = block->getParentModel()->sigModelDestroying.connect(boost::bind(&Model::onModelDestroying, this, _1));
}
//subscribe to this modelblock's destruction. we don't need to track this because modelblocks never point to
//other models and so for its lifetime it will point to us
this->modelBlockDestructionConnections.push_back(block->sigBlockDestroying.connect(boost::bind(&Model::onModelBlockDestroying, this, _1)));
}
void Model::onModelDestroying(IModel *model)
{
if (this->modelDestructionConnections.count(model))
{
this->modelDestructionConnections[model].disconnect();
this->modelDestructionConnections.erase(model);
}
}
void Model::onModelBlockDestroying(IBlock *modelBlock)
{
if (!this->useCounts[modelBlock->getParentModel()])
return; //we've never seen this modelblock before as far as we know
//decrement the the model count
//note that even if the modelblock's parent pointer is invalid, we are just using it for being a value so its ok to use here
this->useCounts[modelBlock->getParentModel()]--;
if (this->useCounts[modelBlock->getParentModel()] <= 0 && this->modelDestructionConnections.count(modelBlock->getParentModel()))
{
//we are no longer used by this model
this->modelDestructionConnections[modelBlock->getParentModel()].disconnect();
this->modelDestructionConnections.erase(modelBlock->getParentModel());
}
}
Here's what happens
Every thing works fine and dandy when I create a bunch of models with nested models inside of them using ModelBlocks. However, I anticipated some problems with destruction, so I braced myself for a giant segfault...which never came. Instead, when I had all the models (and all their blocks) begin the destruction phase, I got a sigabrt which said it happened at Model::onModelBlockDestroying
right at the first if
. I looked at the console and it said pure virtual method called
. I've never seen this error before so I am unsure how to fix it.
The stack trace shows that it was calling the ~IBlock destructor and emitted the sigBlockDestroying
signal which after 10 function levels finally called the onModelBlockDestroying
function. Now, had the Model been destroyed, all of its signals should have been disconnected (see the ~Model
) and I would have thought that the sigBlockDestroying would have called nothing. So, I can conclude that the Model was still in existence when the ~IBlock destructor was called and that the object was still valid. I am 99.9% sure I am wrong in this assumption since obviously there is a problem, but I am not sure why it is happening or how to fix it. I know its a lot of code above, but does anyone see where I am going wrong?
EDIT: I have a feeling it has to do with calling a member function of the IBlock* passed in to onModelBlockDestroying
, but the object hasn't gone away yet (unless, because it already passed through the destructor for the actual implementation it is only left with the pure virtual to call). Is that what is happening? Because the destructor is in ~IBlock, by the time it gets that far down the tree, its already called the destructor for ~ModelBlock and so all the implemented functions are no longer accessible?
If I failed to explain well enough, just let me know and I will clarify.