CNA-BUG-087: Game::Dispose(true) walks Components by live index, so a component that removes itself or another during Dispose makes the loop skip one
Evidence basis: source-verified at the pinned commit. Claims on this page were checked by reading the CNA source at commit 009d40f5; unless a sentence says otherwise, nothing here was built or executed. Nothing on this page was executed unless the Evidence section says so.
XNA 4.0 copies the collection to an array before disposing; CNA re-reads the count and indexes the live collection, so a removal during a component's Dispose shifts later entries and the next component is never disposed by that pass.
- Identifier
CNA-BUG-087- Category
- Bug
- Subsystem
- Core & runtime
- Status
- Open
- Verified against
- CNA
009d40f5(009d40f5dd085c4e674d3479675fac84b12b3e0a) - Severity
- Low (a triage suggestion, not a project priority)
- Evidence basis
- Source-verified: read at TARGET, not executed
- Tests touching this area
- None
- Affected contract
- Microsoft::Xna::Framework::Game::Dispose(bool)
Expected behaviour
XNA 4.0 (IL of Game.Dispose(bool)) copies Components into a new IGameComponent[] and disposes from the copy, which is needed because XNA's GameComponent.Dispose itself removes the component.
Actual behaviour at TARGET
Game.cpp loops for (i = 0; i < Components_.getCountProperty(); ++i) and disposes Components_[i]. If a component's Dispose removes itself (the XNA behaviour a port may re-implement, see CNA-BUG-086) or another component, every later entry shifts down one slot and the component that moved into slot i is skipped.
Source locations
modules/runtime/src/Game.cpp— Game::Dispose(bool) component loop
Evidence
Checked by reading the named sources at 009d40f5; nothing was built or executed for this entry.
Focused reproduction
// Illustrative; not compiled or run for this entry.
class SelfRemoving : public GameComponent {
using GameComponent::GameComponent;
protected:
void Dispose(bool disposing) override {
getGameProperty().getComponentsProperty().Remove(this);
GameComponent::Dispose(disposing);
}
};
// Components = { a (SelfRemoving), b }
// game.Dispose(): a is disposed and removed; b moves to index 0; i becomes 1; b is never disposed.
Current tests
No test disposes a Game whose components mutate the collection.
Regression test
A test with two components where the first removes itself in Dispose, asserting that both were disposed.
Blast radius
Games whose component Dispose overrides remove components (the XNA-faithful pattern). Games with passive components are unaffected.
Workaround
Do not mutate Components from a component's Dispose, or dispose components yourself before the Game.
Related pages
The same subject is explained at several altitudes. These are the neighbouring pages at each one.