I'm writing an A* algorithm for Rush Hour (game) in order to learn the basics. For that I wrote a class named Board, which instances also act as the 'nodes' for the A* search.
For the algorithm to work, I generate (generate_moves()) the possible moves as new Board's and add them to the original board as children (std::priority_queue named children).
For some reason, the moment I decide to obtain a Boardfrom mystd::priority_queue childrencontainer withtop(), the pointer it has towards its parent (Board* parent), which is assigned previously during generation, turns into a pointer towards itself.
- Abbreviated
generate_moves()function:
"for each possible move do"
...
Board newBoard = Board();
...
newBoard.parent = this; // I understand 'this' to always point towards the function caller.
children.push(newBoard); // Priority queue of the current board
...
The following images display the debugging process in the a_star() function (which takes the calling Board as root) after a single node expansion from root, while grabbing the very first 'child':
before top()
As you see, every element inside the open priority queue has the root parent, whose own parent is 'nullptr' (this is correct, root node has no parent).
custom score-comparing class Compare for the priority queues
As you can see, everything is messed up already. From the very first comparison, the parent'parent pointer on every child now goes towards itself, instead of nullptr.
after top() (Board currNode)
After the priority queue exits top(), The node is now messed up, having a self-referencing parent.
after top() (priority_queue open)
The same goes for every Board inside the open priority queue, having a self-referencing parent.
I've been dabbling around this issue for a long time now. Any help would be deeply appreciated. If you require any additional context please let me know.
I've tried:
- Replacing
priority_queuewith my own type, but I end up dealing with the issues of 'const'-required values, for which I end up creating what's basically the same class. - Giving the parent pointer as a constructor parameter. Didn't change a thing.
- Instead of using a copy of the OPEN priority_queue for some comparisons that require popping (I don't want to do that to the original), I also tried popping the original and then pushing the nodes back in, as the 'copying' bit could've been the issue. Didn't change a thing.
- I changed the Compare function from comparing reference values (
Board&) to normal values (Board), as the issue seems to be happening during comparison. Didn't change a thing.
EDIT: Here's my attempt at a "minimal reproducible example". Not sure if consistent due to use of rand() to simplify some functions but it showcases the exact same issue:
#pragma once
#include <iostream>
#include <bitset>
#include <vector>
#include <queue>
#include <stdexcept>
using namespace std;
class SimplifiedBoard {
public:
bitset<36> board;
class Compare {
public:
bool operator() (SimplifiedBoard& a, SimplifiedBoard& b) {
return (a.fscore < b.fscore);
}
};
int fscore;
priority_queue<SimplifiedBoard, vector<SimplifiedBoard>, Compare> children;
SimplifiedBoard* parent;
SimplifiedBoard() {
fscore = 0;
parent = nullptr;
}
// Move generator
void generateMoves() {
int arbitraryChildren = 5;
for (int i = 0; i < arbitraryChildren; i++) {
SimplifiedBoard newBoard = SimplifiedBoard();
newBoard.fscore = (int)rand() / 100;
// Assign parent to child
newBoard.parent = this;
// Add new child
children.push(newBoard);
}
}
// A*
vector<SimplifiedBoard> aStar(SimplifiedBoard& start) {
priority_queue<SimplifiedBoard, vector<SimplifiedBoard>, Compare> open = {};
vector<SimplifiedBoard> closed = {};
start.fscore = 0;
start.parent = nullptr;
open.push(start);
// Loop
while (!open.empty()) {
// ***BUG HAPPENS here (2nd cycle, after root node has been expanded and closed)****
SimplifiedBoard currNode = open.top(); open.pop();
if (currNode.parent != nullptr &&
currNode.parent->parent != nullptr &&
currNode.parent == currNode.parent->parent) {
cout << "Self-referential bug!" << endl;
}
// *** Here, in 2nd cycle, currNode.parent suddenly references a node which references itself ***
// Child generation. f,g,h scores and 'parent' assigned inside function
currNode.generateMoves();
// For each child check OPEN/CLOSE and and see if I add to OPEN
auto childrenCopy = currNode.children;
for (int i = 0; i < (int)currNode.children.size(); i++) {
bool isWorse = false;
SimplifiedBoard currChildNode = childrenCopy.top(); childrenCopy.pop();
// Victory?
if (currChildNode.isVictory()) {
closed.push_back(currChildNode);
// Path reconstruction
vector<SimplifiedBoard> path = {};
SimplifiedBoard currPathNode = currChildNode;
// Ciclo child->parent
while (currPathNode.parent != nullptr) {
path.push_back(currPathNode);
currPathNode = *currPathNode.parent;
}
// Insert root
path.push_back(currPathNode);
// Return path
return path;
}
// OPEN
auto openCopy = open;
for (int j = 0; j < (int)open.size(); j++) {
SimplifiedBoard currOpenNode = openCopy.top(); openCopy.pop();
if (currChildNode.fscore <= currOpenNode.fscore) {
isWorse = true; break;
}
}
if (isWorse) { continue; }
// CLOSED
for (int j = 0; j < (int)closed.size(); j++) {
;
if (currChildNode.fscore <= closed[j].fscore) {
isWorse = true; break;
}
}
if (isWorse) { continue; }
//
open.push(currChildNode);
}
// Close the node
closed.push_back(currNode);
}
// return empty if can't find goal
return vector<SimplifiedBoard>();
}
// Check victory
bool isVictory() {
if ((int)rand() % 50 == 5) { return true; }
else { return false; }
}
};
int main() {
// Debug_example
srand((int)time(NULL));
SimplifiedBoard b = SimplifiedBoard();
cout << "If it gets stuck the bug is happening:" << endl;
vector<SimplifiedBoard> res = b.aStar(b);
for (int i = 0; i < (int)res.size(); i++) {
cout << res[i].fscore << endl;
}
}
Simplifications
Before getting into what's going on, let's simplify your example. In its current form, your question is only applicable to others doing an A*-search, but the problem has nothing to do with searching. Generalizing and simplifying makes the question applicable to a wider audience.
One simplification, that I would expect a question to incorporate, involves realizing that the problem occurs when a node is added to
open. So there is no need to test if a node should be added; it must be added to reproduce the bug, so add it. This greatly simplifies the code. Not only is the chunk of code testing close/open/victory eliminated, but also supporting code likeisVictory()is eliminated. As a corollary, the score (and randomness) can be eliminated – let the priority queue think all nodes are equivalent. (I suppose you could replace the priority queue with a vector at this point, but I'll hold off on that.) Simplify, compile, run, and verify the bug still manifests!There are other simplifications that I think also should have been incorporated into the question's code, but their impact is smaller. (Some are minor enough that I did not bother with them.) There are also some simplifications that I would not expect from someone who does not know the answer. These are helpful, though, so I've incorporated them in the code below. Perhaps the most notable of these simplifications is that I don't store children in the node. To reproduce the bug, it is enough to have a node point to its parent; going from parent to child is not necessary. This makes it reasonable to inline
generateMoves(), a function that helped obscure the situation.To further clarify the situation, I've added some diagnostic output at key locations. (Knowing which locations are key is easier when you know the answer. Still, knowing which locations are potentially key is a useful debugging skill.)
Here is the output:
Analysis
Notice the first two lines of output. Even though
currNodeis a new object each iteration, its address remains the same. While this is not guaranteed, it is typical. This is ingredient one.The second ingredient is the construction of child nodes. What is assigned as the parent of these nodes? If you work through the question's code, you should see that the parent of the children is
currNode(the*thisfor the call togenerateMoves()). In the simplified version, this is easier to see, with the expressionSimplifiedBoard(&currNode). So every child node has its parent set tocurrNode. While this is a different object each iteration, we just saw that its address does not change. Every child is given the same address for its parent. Looking familiar?I should note that the parent pointer becomes a dangling pointer at the end of the current iteration of the loop. That means the behavior of your code is undefined, so your results are not guaranteed. However, your results are typical.
Now to wrap up what happens, think about what happens to
currNodein the second iteration. The top node of the priority queue is copied intocurrNode. In the first iteration, its parent pointer is null, but in the second iteration, its parent pointer is&currNode. This is the loop you see. The parent ofcurrnodeiscurrNode, as demonstrated in the third line of output. Once you realize this, you can simplify your test for the bug. Instead of going tocurrNode.parent->parent, you could simply checkcurrNode.parent == &currNodeand skip the null check.So it's not so much that "the pointer it has towards its parent [...] turns into a pointer towards itself", but that the node is copied to where the parent pointer points. It's not
this->parentthat changes, butthis.Solution
So what can you do about this? The basic problem is that you create short-lived copies, when you need long-lived objects. A solution is to not create the copies.
Rather than creating new objects, your
aStarfunction could use pointers to the existing objects. If object lifetime is a concern, you could usestd::shared_ptr, but given the context, raw (non-owning) pointers are probably sufficient. Just be sure that all children are created before obtaining any of their addresses. Adding and removing elements from a container sometimes (depending on the container) invalidates pointers to other elements of that container.I'll add emphasis on "your
aStarfunction". Theopenandclosedcontainers are what should store pointers instead of objects. What you do with the data members ofSimplifiedBoardis a separate concern. And, yes, when you stop making copies, you will need to go back to storing children in the parent. This is a promising sign.Here is an example of using pointers. This code does not exhibit the self-referential bug (but I left in the diagnostics).