From 8e0979a6aa9e5508f8f0698d22ccb77c9362f982 Mon Sep 17 00:00:00 2001 From: Justin Heyes-Jones Date: Tue, 29 Sep 2026 20:48:19 -0700 Subject: [PATCH] Add AStarSearch RAII destructor, document FSA contract, and add tests - Add ~AStarSearch() to automatically clean up solution or active nodes and ensure UserState destructors execute on scope exit - Document FixedSizeAllocator contract regarding lack of used-list and caller-managed object destruction - Repurpose DestructorTracker in tests.cpp to verify RAII cleanup for both completed and interrupted searches --- fsa.h | 7 +++++++ stlastar.h | 8 ++++++++ tests.cpp | 54 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 69 insertions(+) diff --git a/fsa.h b/fsa.h index c694ce9..12414cf 100644 --- a/fsa.h +++ b/fsa.h @@ -3,6 +3,13 @@ Copyright 2001 Justin Heyes-Jones Simplified for O(1) allocation without used-list overhead. + + NOTE: FixedSizeAllocator is a high-performance, lean pool allocator designed + for fast fixed-size block allocation using a singly-linked free list. + It does NOT maintain a used-list or invoke destructors (~USER_TYPE) on allocated + objects upon its own destruction. Callers are responsible for pairing every alloc() + with an explicit free() (and invoking object destructors where applicable) before + the allocator is destroyed. Double-freeing an element will corrupt the free list. */ #ifndef FSA_H diff --git a/stlastar.h b/stlastar.h index ce5b1e8..39a91ca 100644 --- a/stlastar.h +++ b/stlastar.h @@ -130,6 +130,14 @@ class AStarSearch { m_CurrentExpandingNode(nullptr) { } + ~AStarSearch() { + if (m_State == SEARCH_STATE_SUCCEEDED) { + FreeSolutionNodes(); + } else if (m_Start != nullptr) { + FreeAllNodes(); + } + } + // call at any time to cancel the search and free up all the memory void CancelSearch() { m_CancelRequest = true; diff --git a/tests.cpp b/tests.cpp index 12eff38..98f4d64 100644 --- a/tests.cpp +++ b/tests.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include "stlastar.h" @@ -275,6 +276,27 @@ struct DestructorTracker { }; int DestructorTracker::alive_count = 0; +struct TrackedUserState { + int id; + std::shared_ptr tracker; + + TrackedUserState() : id(0), tracker(std::make_shared()) {} + TrackedUserState(int _id) : id(_id), tracker(std::make_shared()) {} + + float GoalDistanceEstimate(TrackedUserState& goal) { return (float)std::abs(id - goal.id); } + bool IsGoal(TrackedUserState& goal) { return id == goal.id; } + bool GetSuccessors(AStarSearch* astar, TrackedUserState* parent) { + if (id < 3) { + TrackedUserState next(id + 1); + astar->AddSuccessor(next); + } + return true; + } + float GetCost(TrackedUserState& successor) { return 1.0f; } + bool IsSameState(TrackedUserState& rhs) { return id == rhs.id; } + size_t Hash() { return std::hash{}(id); } +}; + class ReopenTestNode { public: @@ -356,4 +378,36 @@ TEST_CASE("Start Node Equals Goal Node") { astar.EnsureMemoryFreed(); } +TEST_CASE("AStarSearch Destructor Cleans Up Live Nodes On Scope Exit Without Manual Free") { + DestructorTracker::alive_count = 0; + { + AStarSearch astar; + TrackedUserState start(0); + TrackedUserState goal(2); + astar.SetStartAndGoalStates(start, goal); + + unsigned int state; + do { + state = astar.SearchStep(); + } while (state == AStarSearch::SEARCH_STATE_SEARCHING); + + CHECK(state == AStarSearch::SEARCH_STATE_SUCCEEDED); + // FreeSolutionNodes() is intentionally NOT called here to test destructor RAII cleanup + } + CHECK(DestructorTracker::alive_count == 0); +} + +TEST_CASE("AStarSearch Destructor Cleans Up Unfinished Search On Scope Exit") { + DestructorTracker::alive_count = 0; + { + AStarSearch astar; + TrackedUserState start(0); + TrackedUserState goal(2); + astar.SetStartAndGoalStates(start, goal); + // Execute only one step; search remains in progress + astar.SearchStep(); + } + CHECK(DestructorTracker::alive_count == 0); +} +