I managed to resolve this myself. The bug was in the way I was creating the Hero object.
Instead of static_cast'ing an existing Node into a Hero, I followed the pattern that CCSprite uses to subclass Node, by doing the following in Hero instead:
// Hero.cpp
// old fn header:
// Hero* Hero::create(Node* heroNode, const ValueMap& configuration) {
Hero* Hero::create(const ValueMap& configuration, const Vec2& startPosition) {
// old code:
// Hero* hero = static_cast<Hero*>(heroNode)
Hero* hero = new Hero();
if(hero && hero->init(configuration, startPosition)) {
// ... rest is the same
}
// ctor is empty
Hero::Hero() {}
// initializer
bool Hero::init(const ValueMap& configuration, const Vec2& startPosition) {
// Node is created here
if(!Node::init()) {
FATAL("Could not create the Hero's Node!");
return false;
}
// the only reason I really needed that `heroNode` was to be able to
// position a Node visually in Cocos Studio, and re-use the same
// Node in the code to save a few ops
//
// Now I just do that manually here:
setPosition(startPosition);
// other config
return true;
}
I still don’t fully understand what C++ was doing to cause it (my best guess right now is that it has something to do with ‘copy elision’ - link).
Something about static_cast doesn’t actually convert the pointer to the derived class. So that caused any extra member variables that don’t exist in the base class, Node, to fall out of scope and get leaked. Which would explain why they still exist in the AutoReleasePool (since they haven’t been explicitly released yet), but don’t exist as a reference in the fake Hero* pointer.
The XCode Leaks Instrument also tells me there are no leaks now, which is good.
If you have more of an idea as to why this solution works, do let me know!