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

CNA snapshot 009d40f5  ·  Known Issues › Current bugs  ·  source links pinned to 009d40f5

✓

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

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.

The same subject is explained at several altitudes. These are the neighbouring pages at each one.

Known issues
Bug index