c++ 为什么'std::priority_queue.top()'更改返回的对象成员值?

roejwanj  于 2023-07-01  发布在  其他
关注(0)|答案(1)|浏览(111)

我正在为《尖峰时刻》(游戏)写一个A* 算法,以便学习基础知识。为此,我编写了一个名为Board的类,其示例也充当A* 搜索的“节点”。
为了让算法工作,我生成(generate_moves())可能的移动作为新的Board,并将它们作为子节点添加到原始棋盘(std::priority_queue命名为children)。
出于某种原因,当我决定从std::priority_queue children容器中获取Board时,它指向父对象(Board* parent)的指针(在生成过程中分配)变成了指向它自己的指针。
1.缩写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扩展单个节点后,在a_star()函数(以调用Board为root)中调试过程,同时抓取第一个'child':before top()正如你所看到的,open优先级队列中的每个元素都有根父节点,它自己的父节点是'nullptr'(这是正确的,根节点没有父节点)。
custom score-comparing class Compare for the priority queues正如你所看到的,一切都已经一团糟了。从第一次比较开始,父节点的parent指针指向每个子节点,而不是nullptr
after top() (Board currNode)在优先级队列退出top()之后,节点现在混乱了,有一个自引用的父节点。
after top() (priority_queue open)open优先级队列中的每个Board也是如此,它有一个自引用父级。
我一直在涉足这个问题很长一段时间了。任何帮助都将不胜感激。如果您需要任何额外的背景,请让我知道。
我试过了

  • 用我自己的类型替换priority_queue,但我最终处理了“const”必需值的问题,为此我最终创建了基本相同的类。
  • 给出父指针作为构造函数参数。什么都没改变。
  • 我没有使用OPEN priority_queue的副本来进行一些需要弹出的比较(我不想对原始的进行这样的比较),而是尝试弹出原始的,然后将节点推回,因为“复制”位可能是问题所在。什么都没改变。
  • 我将比较函数从比较参考值(Board&)改为比较正常值(Board),因为问题似乎发生在比较过程中。什么都没改变。

编辑:这是我尝试的一个“最小可重复的例子”。不确定是否一致,因为使用兰德()来简化一些函数,但它展示了完全相同的问题:

#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;
    }
}
mzillmmw

mzillmmw1#

简化

在深入了解发生了什么之前,让我们简化一下示例。在目前的形式下,您的问题仅适用于其他进行A* 搜索的人,但问题与搜索无关。概括和简化使问题适用于更广泛的受众。
一个简化(我希望在问题中包含)涉及到当将节点添加到open时出现问题。因此不需要测试 * 是否 * 应该添加一个节点;它 * 必须 * 被添加以重现bug,所以添加它。这大大简化了代码。不仅消除了测试close/open/victory的代码块,而且还消除了像isVictory()这样的支持代码。作为推论,可以消除得分(和随机性)-让优先级队列认为所有节点都是等效的。(我想你可以用一个向量来代替优先级队列,但是我会推迟这个步骤。)简化、编译、运行,并验证bug仍然存在!
我认为还有其他一些简化也应该被纳入问题的代码中,但它们的影响较小。(有些是足够小的,我没有费心与他们。)也有一些简化,我不会期望从一个人谁不知道答案。不过,这些都很有用,所以我将它们合并到下面的代码中。也许这些简化中最值得注意的是,我没有在节点中存储子节点。要重现bug,只要有一个节点指向其父节点就足够了;从父母到孩子是不必要的。这使得内联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(调用generateMoves()*this)。在简化版本中,这更容易看到,表达式为SimplifiedBoard(&currNode)。因此,每个子节点都将其父节点设置为currNode。虽然每次迭代都是不同的对象,但我们刚刚看到它的地址没有改变。每个子节点都被赋予与其父节点相同的地址。* 看起来很眼熟?*

  • 我应该注意到,父指针在循环的当前迭代结束时变成了一个悬空指针。这意味着代码的行为是未定义的,因此不能保证结果。但是,您的结果是典型的。*

现在总结一下发生了什么,想想在第二次迭代中currNode发生了什么。优先级队列的顶部节点被复制到currNode中。在第一次迭代中,其父指针为null,但在第二次迭代中,其父指针为&currNode。这就是你看到的循环。currnode的父代是currNode,如输出的第三行所示。* 一旦你意识到这一点,你就可以简化你对bug的测试。您可以直接检查currNode.parent == &currNode,而不是转到currNode.parent->parent,并跳过null检查。
因此,这并不是说“它指向父节点[...]的指针变成了指向它自己的指针”,而是节点被复制到父节点指针指向的地方。改变的不是this->parent,而是this

解决方案

那么你能做些什么呢?基本的问题是,当您需要长寿命的对象时,您创建了短寿命的副本。解决方案是不创建副本。
aStar函数可以使用指向现有对象的指针,而不是创建新对象。如果对象生存期是一个问题,您可以使用std::shared_ptr,但给定上下文,原始(非拥有)指针可能就足够了。只需确保在获取任何子地址之前创建了所有子地址。在容器中添加和删除元素有时会使指向该容器中其他元素的指针失效(具体取决于容器)。
我将强调“您的aStar函数”。openclosed容器应该存储指针而不是对象。如何处理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);
    }
};

// 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";
}

相关问题