提问人:Gabriel Balassa Turu 提问时间:6/17/2023 最后编辑:Gabriel Balassa Turu 更新时间:7/23/2023 访问量:152
为什么 'std::p riority_queue.top()' 会更改返回的对象成员值?
Why is 'std::priority_queue.top()' changing returned object member values?
问:
我正在为尖峰时刻(游戏)编写一个 A* 算法,以便学习基础知识。为此,我编写了一个名为 的类,该类的实例也充当 A* 搜索的“节点”。Board
为了使算法起作用,我将 () 可能的移动生成为新移动,并将它们作为子项(命名为 )。generate_moves()
Board
std::priority_queue
children
出于某种原因,当我决定从 mycontainer 中获取 时,它指向其父项 () 的指针(之前在生成过程中分配)变成了指向自身的指针。Board
std::priority_queue children
top()
Board* parent
- 缩写功能:
generate_moves()
"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
...
下图显示了从 root 进行单个节点扩展后函数(以 root 身份调用)中的调试过程,同时抓取第一个“子节点”: 在 top() 之前 如您所见,优先级队列中的每个元素都有根父节点,其自己的父节点是“nullptr”(这是正确的,根节点没有父节点)。a_star()
Board
open
自定义分数比较类 比较
优先级队列正如你所看到的,一切都已经搞砸了。从第一次比较开始,每个子项上的父项指针现在都指向自身,而不是 。parent
nullptr
after top() (板 currNode)在优先级队列退出后,该节点现在被搞砸了,有一个自引用的父节点。top()
after top() (priority_queue打开)优先级队列中的每个优先级队列也是如此,有一个自引用的父级。Board
open
我已经涉足这个问题很长时间了。任何帮助将不胜感激。如果您需要任何其他背景信息,请告诉我。
我试过:
- 替换为我自己的类型,但我最终处理了“const”必需值的问题,为此我最终创建了基本相同的类。
priority_queue
- 将父指针作为构造函数参数提供。什么都没改变。
- 我没有使用 OPEN priority_queue的副本进行一些需要弹出的比较(我不想对原始节点这样做),我还尝试弹出原始节点,然后将节点推回,因为“复制”位可能是问题。什么都没改变。
- 我将比较功能从比较参考值 () 更改为正常值 (),因为在比较过程中似乎发生了问题。什么都没改变。
Board&
Board
编辑:这是我对“最小可重复示例”的尝试。由于使用 rand() 来简化某些函数,不确定是否一致,但它展示了完全相同的问题:
#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;
}
}
答:
简化
在开始讨论正在发生的事情之前,让我们简化您的示例。在目前的形式下,您的问题仅适用于进行 A* 搜索的其他人,但该问题与搜索无关。概括和简化使问题适用于更广泛的受众。
我希望一个问题包含一种简化,即意识到当将节点添加到 时会出现问题。因此,无需测试是否应该添加节点;必须添加它才能重现错误,因此请添加它。这大大简化了代码。不仅消除了代码测试关闭/开放/胜利的块,而且还消除了支持代码之类的代码。作为推论,分数(和随机性)可以消除——让优先级队列认为所有节点都是等价的。(我想此时你可以用向量替换优先级队列,但我会推迟。简化、编译、运行和验证错误仍然存在!open
isVictory()
还有其他简化,我认为也应该合并到问题的代码中,但它们的影响较小。(有些是微不足道的,我没有理会它们。还有一些简化,我不会期望不知道答案的人。不过,这些很有帮助,因此我将它们合并到下面的代码中。也许这些简化中最值得注意的一点是,我没有在节点中存储子项。要重现该错误,只需让一个节点指向其父节点就足够了;没有必要从父母到孩子。这使得内联变得合理,这个函数有助于掩盖这种情况。generateMoves()
为了进一步澄清情况,我在关键位置添加了一些诊断输出。(当你知道答案时,知道哪些位置是关键的会更容易。不过,知道哪些位置可能是关键的,是一项有用的调试技能。
#include <iostream>
#include <queue>
using namespace std; // <-- Not recommended, but I'll leave it alone
class SimplifiedBoard {
public:
bool operator< (const SimplifiedBoard&) const {
return false;
}
SimplifiedBoard* parent;
SimplifiedBoard(SimplifiedBoard* parent = nullptr) : parent(parent) {}
// Return true on success, false on the bug.
bool aStar(SimplifiedBoard& start) {
// Initial setup
priority_queue<SimplifiedBoard> open = {};
open.push(start);
// Loop (with an arbitrary cap)
while (!open.empty() && open.size() < 50) {
// Remove the top of the priority queue.
SimplifiedBoard currNode = open.top(); open.pop();
std::cout << "Address of currNode: " << &currNode << "\n";
// ***BUG seen here****
if (currNode.parent != nullptr &&
currNode.parent == currNode.parent->parent) {
std::cout << "Address of parent: " << currNode.parent << "\n";
return false;
}
// Add a child to OPEN.
open.push(SimplifiedBoard(&currNode));
}
return true;
}
};
int main() {
SimplifiedBoard b;
if (b.aStar(b))
cout << "Success!\n";
else
cout << "Self-referential bug!\n";
}
输出如下:
Address of currNode: 0x7fff3a547408
Address of currNode: 0x7fff3a547408
Address of parent: 0x7fff3a547408
Self-referential bug!
分析
请注意输出的前两行。即使每次迭代都是一个新对象,其地址也保持不变。虽然这不能保证,但这是典型的。这是成分一。currNode
第二个要素是子节点的构造。什么被分配为这些节点的父节点?如果您仔细处理问题的代码,您应该看到子项的父项是(用于调用 )。在简化版本中,使用表达式 .因此,每个子节点的父节点都设置为 。虽然每次迭代都是一个不同的对象,但我们只是看到它的地址没有改变。每个孩子都为其父母提供相同的地址。看起来很眼熟?currNode
*this
generateMoves()
SimplifiedBoard(&currNode)
currNode
我应该注意,父指针在循环的当前迭代结束时变成了一个悬空指针。这意味着代码的行为是未定义的,因此无法保证结果。但是,您的结果是典型的。
现在总结一下发生了什么,想想在第二次迭代中会发生什么。优先级队列的顶层节点将复制到 中。在第一次迭代中,其父指针为 null,但在第二次迭代中,其父指针为 。这是你看到的循环。is 的父级 ,如输出的第三行所示。一旦你意识到这一点,你就可以简化对错误的测试。无需转到 currNode.parent->
parent,只需检查 currNode.parent == &currNode
并跳过 null 检查即可。currNode
currNode
&currNode
currnode
currNode
因此,与其说是“它对父级的指针 [...]变成指向自身的指针“,但节点被复制到父指针指向的位置。不是改变,而是.this->parent
this
溶液
那么你能做些什么呢?基本问题是,当您需要长期存在的对象时,您会创建短期副本。解决方案是不创建副本。
函数可以使用指向现有对象的指针,而不是创建新对象。如果对象生存期是一个问题,您可以使用 ,但考虑到上下文,原始(非拥有)指针可能就足够了。只需确保在获取其任何地址之前创建所有子项即可。在容器中添加和删除元素有时会(取决于容器)使指向该容器的其他元素的指针失效。aStar
std::shared_ptr
我将强调“您的功能”。和容器应该存储指针而不是对象。如何处理其数据成员是一个单独的问题。而且,是的,当您停止制作副本时,您将需要回到将孩子存储在父项中。这是一个有希望的迹象。aStar
open
closed
SimplifiedBoard
下面是使用指针的示例。此代码不会显示自引用错误(但我在诊断中留下了)。
#include <iostream>
#include <vector>
struct Node {
Node* parent = nullptr;
std::vector<Node> children; // <-- Not necessarily pointers
void generateMoves() {
// Only one child in this example
children.emplace_back(this);
// If not on a recent enough compiler version and C++20:
// children.emplace_back(Node{.parent = this, .children = {}});
}
};
// Return true on success, false on the bug.
bool aStar(Node& start) {
std::vector<Node*> open{&start}; // <-- Pointers
// Loop (with an arbitrary cap)
while (open.size() < 50) {
Node* currNode = open.back();
std::cout << "Address in currNode: " << currNode << "\n";
// ***BUG was seen here****
if (currNode->parent == currNode) {
std::cout << "Address of parent: " << currNode->parent << "\n";
return false;
}
// Add a child to OPEN.
currNode->generateMoves();
open.emplace_back(&currNode->children[0]);
}
return true;
}
int main() {
Node b;
if (aStar(b))
std::cout << "Success!\n";
else
std::cout << "Self-referential bug!\n";
}
评论
Error C2664 'Node::Node(const Node &)': el argumento 1 no puede convertirse de '_Ty' a 'const Node &
this
Node()
children.emplace_back(Node{.parent = this, .children = {}});
评论
Board
Fscores: