为什么 'std::p riority_queue.top()' 会更改返回的对象成员值?

Why is 'std::priority_queue.top()' changing returned object member values?

提问人:Gabriel Balassa Turu 提问时间:6/17/2023 最后编辑:Gabriel Balassa Turu 更新时间:7/23/2023 访问量:152

问:

我正在为尖峰时刻(游戏)编写一个 A* 算法,以便学习基础知识。为此,我编写了一个名为 的类,该类的实例也充当 A* 搜索的“节点”。Board

为了使算法起作用,我将 () 可能的移动生成为新移动,并将它们作为子项(命名为 )。generate_moves()Boardstd::priority_queuechildren

出于某种原因,当我决定从 mycontainer 中获取 时,它指向其父项 () 的指针(之前在生成过程中分配)变成了指向自身的指针。Boardstd::priority_queue childrentop()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 进行单个节点扩展后函数(以 root 身份调用)中的调试过程,同时抓取第一个“子节点”: 在 top() 之前 如您所见,优先级队列中的每个元素都有根父节点,其自己的父节点是“nullptr”(这是正确的,根节点没有父节点)。a_star()Boardopen

自定义分数比较类 比较优先级队列正如你所看到的,一切都已经搞砸了。从第一次比较开始,每个子项上的父项指针现在都指向自身,而不是 。parentnullptr

after top() (板 currNode)在优先级队列退出后,该节点现在被搞砸了,有一个自引用的父节点。top()

after top() (priority_queue打开)优先级队列中的每个优先级队列也是如此,有一个自引用的父级。Boardopen

我已经涉足这个问题很长时间了。任何帮助将不胜感激。如果您需要任何其他背景信息,请告诉我。

我试过:

  • 替换为我自己的类型,但我最终处理了“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;
    }
}
C++ 这个 无限循环 优先级队列 自引用

评论

3赞 Retired Ninja 6/17/2023
你能把一个最小的可重现的例子放在一起来证明这个问题吗?
0赞 Gabriel Balassa Turu 6/17/2023
@RetiredNinja我会努力的。
0赞 o_oTurtle 6/17/2023
是否有任何其他构造函数,例如移动 ctor/copy ctor?Board
0赞 Gabriel Balassa Turu 6/17/2023
@o_oTurtle 没有。只有初始值的构造函数。我将在一秒钟内添加一个 MRE。
0赞 JaMiT 6/17/2023
我从您的示例中得到的输出是 .预期输出是多少?这如何证明所描述的问题?您能否通过更改输出来更好地集中您的示例代码,以演示“它指向其父级的指针 [...]变成指向自身的指针“,而不是展示您的 A* 搜索结果?Fscores:

答:

1赞 JaMiT 6/30/2023 #1

简化

在开始讨论正在发生的事情之前,让我们简化您的示例。在目前的形式下,您的问题仅适用于进行 A* 搜索的其他人,但该问题与搜索无关。概括和简化使问题适用于更广泛的受众。

我希望一个问题包含一种简化,即意识到当将节点添加到 时会出现问题。因此,无需测试是否应该添加节点;必须添加它才能重现错误,因此请添加它。这大大简化了代码。不仅消除了代码测试关闭/开放/胜利的块,而且还消除了支持代码之类的代码。作为推论,分数(和随机性)可以消除——让优先级队列认为所有节点都是等价的。(我想此时你可以用向量替换优先级队列,但我会推迟。简化、编译、运行和验证错误仍然存在!openisVictory()

还有其他简化,我认为也应该合并到问题的代码中,但它们的影响较小。(有些是微不足道的,我没有理会它们。还有一些简化,我不会期望不知道答案的人。不过,这些很有帮助,因此我将它们合并到下面的代码中。也许这些简化中最值得注意的一点是,我没有在节点中存储子项。要重现该错误,只需让一个节点指向其父节点就足够了;没有必要从父母到孩子。这使得内联变得合理,这个函数有助于掩盖这种情况。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*thisgenerateMoves()SimplifiedBoard(&currNode)currNode

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

现在总结一下发生了什么,想想在第二次迭代中会发生什么。优先级队列的顶层节点将复制到 中。在第一次迭代中,其父指针为 null,但在第二次迭代中,其父指针为 。这是你看到的循环。is 的父级 ,如输出的第三行所示。一旦你意识到这一点,你就可以简化对错误的测试。无需转到 currNode.parent->parent,只需检查 currNode.parent == &currNode 并跳过 null 检查即可。currNodecurrNode&currNodecurrnodecurrNode

因此,与其说是“它对父级的指针 [...]变成指向自身的指针“,但节点被复制到父指针指向的位置。不是改变,而是.this->parentthis

溶液

那么你能做些什么呢?基本问题是,当您需要长期存在的对象时,您会创建短期副本。解决方案是不创建副本。

函数可以使用指向现有对象的指针,而不是创建新对象。如果对象生存期是一个问题,您可以使用 ,但考虑到上下文,原始(非拥有)指针可能就足够了。只需确保在获取任何地址之前创建所有子项即可。在容器中添加和删除元素有时会(取决于容器)使指向该容器的其他元素的指针失效。aStarstd::shared_ptr

我将强调“您的功能”。和容器应该存储指针而不是对象。如何处理其数据成员是一个单独的问题。而且,是的,当您停止制作副本时,您将需要回到将孩子存储在父项中。这是一个有希望的迹象。aStaropenclosedSimplifiedBoard

下面是使用指针的示例。此代码不会显示自引用错误(但我在诊断中留下了)。

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

评论

0赞 Gabriel Balassa Turu 7/22/2023
谢谢你的回答。虽然我不是 C++ 的绝对新手,但当来自垃圾收集语言时,真的很难习惯指针,特别是当问题被标准容器掩盖时(人们通常会自由滥用它,就好像垃圾回收从未消失,指针也不是一回事)。
0赞 Gabriel Balassa Turu 7/22/2023
注意:由于关键字,后一个“固定”代码向我抛出了一个错误。不太清楚为什么。将其替换为 .Error C2664 'Node::Node(const Node &)': el argumento 1 no puede convertirse de '_Ty' a 'const Node &thisNode()
0赞 JaMiT 7/23/2023
@GabrielBalassaTuru “后一个'固定'代码向我抛出一个 [错误]”——可能是您的编译器对 C++20 的支持不完整,或者它正在为早期标准进行编译。(快速检查表明此代码需要 gcc 10 或 clang 17。C++17 版本是 .children.emplace_back(Node{.parent = this, .children = {}});
0赞 Gabriel Balassa Turu 7/24/2023
右。我仍然在使用 VS2019。