具有递归数据结构的 Boost 序列化最终导致堆栈溢出

Posted

技术标签:

【中文标题】具有递归数据结构的 Boost 序列化最终导致堆栈溢出【英文标题】:Boost serialization with recursive data structure ends up in stack overflow 【发布时间】:2022-01-06 11:06:29 【问题描述】:

问题

我在使用具有递归数据结构的 Boost 序列化库时遇到问题。更准确地说,我想序列化一个矩阵,该矩阵由包含一个值的节点表示,并且每个节点都可以访问其邻居(顶部、底部、左侧、右侧)。为了访问一个节点,每个入口点都存储在一个vector 中(即每行和每列的第一个和最后一个节点)。这是Node 类:

class Node

private:
    int v;
    Node* left; 
    Node* right;
    Node* top;
    Node* bottom;

public:
    Node() : v(rand() % 100), left(NULL), right(NULL), top(NULL), bottom(NULL)
    

    

    //Potential memory leak but that's not the topic
    void setLeft(Node* toSet)  left = toSet; 
    void setRight(Node* toSet)  right = toSet; 
    void setTop(Node* toSet)  top = toSet; 
    void setBottom(Node* toSet)  bottom = toSet; 

    Node* gLeft()  return left; 
    Node* gRight()  return right; 
    Node* gTop()  return top; 
    Node* gBottom()  return bottom; 

    int gValue()  return v; 

    template<class Archive>
    void serialize(Archive& ar, const unsigned int version)
    
        ar& v;
        ar& left;
        ar& right;
        ar& top;
        ar& bottom;
    
;

这是Matrix 类,在这个例子中带有generateValues() 函数:

class Matrix

private:
    int m, n;
    std::vector<Node*> firstNodesPerRow;
    std::vector<Node*> lastNodesPerRow;
    std::vector<Node*> firstNodesPerCol;
    std::vector<Node*> lastNodesPerCol;
public:
    Matrix(int m, int n) : 
        m(m), n(n),
        firstNodesPerRow(m, NULL), lastNodesPerRow(m,NULL),
        firstNodesPerCol(n, NULL),lastNodesPerCol(n, NULL)
    
        
    

    void generateValues()
    
        for (int i = 0; i < m; i++)
        
            for (int j = 0; j < n; j++)
            
                Node* toWrite = new Node();
                if (i > 0)
                
                    toWrite->setTop(lastNodesPerCol.at(j));
                    lastNodesPerCol.at(j)->setBottom(toWrite);
                    lastNodesPerCol.at(j) = toWrite;
                
                else
                
                    firstNodesPerCol.at(j) = toWrite;
                    lastNodesPerCol.at(j) = toWrite;
                
                if (j > 0)
                
                    toWrite->setLeft(lastNodesPerRow.at(i));
                    lastNodesPerRow.at(i)->setRight(toWrite);
                    lastNodesPerRow.at(i) = toWrite;
                
                else
                
                    firstNodesPerRow.at(i) = toWrite;
                    lastNodesPerRow.at(i) = toWrite;
                
            
        
    

    template<class Archive>
    void serialize(Archive& ar, const unsigned int version)
    
        ar& m;
        ar& n;
        ar& firstNodesPerRow;
        ar& firstNodesPerCol;
        ar& lastNodesPerRow;
        ar& lastNodesPerCol;
    
;

所以我想要实现的是序列化和反序列化Matrix。这是我的main 函数:

#include <cstdlib>
#include <sstream>
#include <vector>
#include <boost/archive/text_oarchive.hpp>
#include <boost/archive/text_iarchive.hpp>
#include <boost/serialization/vector.hpp>

int main(int argc, char* argv[])

    int m = 10; int n = 10;
    Matrix toSerialize(m,n);
    toSerialize.generateValues();

    /*
        1) Serialize
    */
    std::ostringstream oss;
    boost::archive::text_oarchive oa(oss);
    oa << toSerialize;

    std::string serialiazedData = oss.str();

    /*
        2) Deserialize
    */
    Matrix result(m,n);
    std::stringstream serializedDataStream(serialiazedData);
    boost::archive::text_iarchive ia(serializedDataStream);
    ia >> result;

    return EXIT_SUCCESS;

问题如下:给定一个足够大的值mnmainstack-overflow 异常结束。我知道它来自Nodeserialize 方法,因为为了序列化一个节点,它需要序列化邻居等等......我发现this topic 这似乎是完全相同的问题。作为起点,答案很有趣,但我很难实现它,因为它不够精确。我的理解是,要解决我的问题,我需要:

    以迭代的方式对节点进行序列化,这样当涉及到邻居时,对象已经被序列化并且没有stack-overflow; 序列化拓扑,在我的例子中通过Node的指针top,bottom,right,left表示

我在实际实施这个解决方案时遇到了麻烦,因为我能想到的唯一方法是在 serializeserialize 方法中删除 top,bottom,right,left 的序列化 Node,但我可以'没有达到第 2 点?

编辑:我制作了一个矩阵图来帮助阅读。请注意,m x n 矩阵中不一定有 m x n 节点。

编辑 2:我想到的解决方案(不起作用,反序列化时最终导致堆栈溢出)。

//Class Node
template<class Archive>
void serialize(Archive& ar, const unsigned int version)

    ar& v;


//Class Matrix
template<class Archive>
void serialize(Archive& ar, const unsigned int version)

    ar& m;
    ar& n;

    for (int i = 0; i < m; i++)
    
        Node* current = firstNodesPerRow.at(i);
        while (1)
        
            if (current == NULL)  break; 
            ar& current;
            current = current->gRight();
        
    

    ar& firstNodesPerRow;
    ar& firstNodesPerCol;
    ar& lastNodesPerRow;
    ar& lastNodesPerCol;


解决方案

解决方案的解释在标记为答案的帖子中给出。这是此解决方案的实现:

// class Node
template<class Archive>
void serialize(Archive& ar, const unsigned int version)

    ar& v;


// some buffer struct
struct Neighbors

    Node* top;
    Node* bottom;
    Node* left;
    Node* right;

    template <typename Archive>
    void serialize(Archive& ar, const unsigned int version)
    
        ar& top;
        ar& bottom;
        ar& left;
        ar& right;
    
;

//class Matrix
template<class Archive>
void save(Archive& ar, const unsigned int version) const

    std::map<Node*, Neighbors> neighborsPerNode;

    for (int i = 0; i < m; i++)
    
        Node* current = firstNodesPerRow.at(i);
        while (1)
        
            if (current == NULL)  break; 
            neighborsPerNode[current] = 
                current->gTop(),
                current->gBottom(),
                current->gLeft(),
                current->gRight(),
            ;
            current = current->gRight();
        
    

    ar& neighborsPerNode;

    ar& m;
    ar& n;

    ar& firstNodesPerRow;
    ar& firstNodesPerCol;
    ar& lastNodesPerRow;
    ar& lastNodesPerCol;

template<class Archive>
void load(Archive& ar, const unsigned int version)

    // Warning ALL the nodes are browsed 2 times :
    //  1 - to deserialize neighborsPerNode (below)
    //  2 - in the for loop, to create the links between the nodes
    
    std::map<Node*, Neighbors> neighborsPerNode;
    ar& neighborsPerNode;

    ar& m;
    ar& n;

    ar& firstNodesPerRow;
    ar& firstNodesPerCol;
    ar& lastNodesPerRow;
    ar& lastNodesPerCol;

    for (int i = 0; i < m; i++)
    
        Node* current = firstNodesPerRow.at(i);
        while (1)
        
            if (current == NULL)  break; 
            Neighbors myNeighbors = neighborsPerNode.at(current);
            current->setTop(myNeighbors.top);
            current->setBottom(myNeighbors.bottom);
            current->setLeft(myNeighbors.left);
            current->setRight(myNeighbors.right);
            current = current->gRight();    
        
    

BOOST_SERIALIZATION_SPLIT_MEMBER()

【问题讨论】:

“我在实际实现这个解决方案时遇到了麻烦,因为我能想到的唯一方法是第 1 点意味着删除顶部、底部、右侧、左侧的序列化” - 我认为这是一个错误的假设。你能对照boost.org/doc/libs/1_76_0/libs/serialization/doc/…检查你的想法吗? “按地址跟踪对象”正是我计划使用的机制。问题是,如果我将top,bottom,right,left 留在serializeNode,那么我需要预先序列化这些指针引用的对象。为此,我可以在Matrixserialize 方法中逐行逐节点浏览矩阵,并在那里序列化每个节点。如果我将top,bottom,right,left 留在Nodeserialize 中,那么第一个节点的序列化将遇到完全相同的问题,因为邻居尚未序列化。我将在 OP 中添加我的代码以使其更清晰。 请注意,在 C++ 中您应该使用 nullptr 而不是 NULL 主要问题是序列化 top,bottom,right,left 会导致归档序列化追逐试图序列化所有内容的指针。 ar&amp; firstNodesPerRow; 被序列化的第一个节点将尝试序列化其直接邻居,而那些尝试序列化其直接邻居等...所以在最坏的情况下,您最终会得到n*m 的递归调用深度。一个简单的解决方案是仅序列化值并在需要再次加载时重新创建节点结构。 我也不太明白如何在示例中省略值。想象一个只有中心元素被设置的 3x3 矩阵。 firstNodesPerRowlastNodesPerRow 等...都只包含零指针,因此无法保存该节点。所以矩阵的中心总会有一个盲点,如果不先填充几个额外的节点,就不能直接添加节点。 【参考方案1】:

为什么不简单地将所有元素按矩阵顺序序列化并完全避免函数调用递归,例如:

template<class Archive>
void serialize(Archive& ar, const unsigned int version)

    ar& m;
    ar& n;
    for (int i = 0; i < m; ++i)
    
        Node* node = firstNodesPerRow[i];
        for (int j = 0; j < n; ++j)
        
            ar & node->gValue();
            node = node->gRight();
        
    

顺便说一句,这适用于保存矩阵。我认为您需要在保存和加载版本中专门化序列化功能,因为对于加载版本,您需要:

    加载 n, m 分配所有节点并在矩阵中填充节点指针向量 以与保存期间相同的顺序加载所有值
    template<class Archive>
    void save(Archive & ar, const unsigned int version) const
    
        ...
    
    template<class Archive>
    void load(Archive & ar, const unsigned int version)
    
        ...
    
    BOOST_SERIALIZATION_SPLIT_MEMBER()

在节点可能丢失的复杂情况下,同样的想法也适用。而且您仍然需要在保存/加载之间进行拆分,为加载添加分配。 但是要正确保存和再次加载需要更多的簿记。

例如,您可以像上面一样首先遍历所有节点,但创建一个从每个节点指针值到唯一递增 ID 号的映射。当您保存每个节点的值时(如上逐行),还要保存每个方向的节点 ID。加载时,首先制作一个空映射:ID -> 节点指针。然后再次逐行恢复节点,同时从映射中恢复邻居指针。当然,每当第一次遇到 ID 时,您都需要分配一个新节点。

【讨论】:

我也有类似的想法,但问题在于“你需要分配所有节点”,因为没有办法先验地知道矩阵中的节点数量(不一定m x n,因此是节点):/。一种可能的解决方法是将节点的数量存储在矩阵中,但您仍然不知道矩阵的“结构”> 这是否意味着您实际上会违反您绘制的图形?它可以与 firstnodespercol.size() 大小为 firstnodesperrow.size() 的矩阵形状不同吗? 矩阵的大小总是firstnodesperrow.size() x firstnodespercol.size(),但是有些节点可以为NULL,在这种情况下没有m x n 节点。假设行i 为空,那么firstNodesPerRow.at(i) 为NULL。你说得对,这个图表可能会让人们感到困惑,我会更新它。 @Spazz 我添加了一个解释,它太大而无法发表评论。 感谢您的解释!这确实是一个解决方案,我将在 Original Post 中添加我的实现。

以上是关于具有递归数据结构的 Boost 序列化最终导致堆栈溢出的主要内容,如果未能解决你的问题,请参考以下文章

WPF UserControl:使多个链接的依赖项属性保持同步,而不会导致递归循环 堆栈溢出

堆栈(线性表)

为啥递归调用会导致不同堆栈深度的 ***?

共享指针递归删除递归数据结构,堆栈溢出

为啥增加递归深度会导致堆栈溢出错误?

由于递归方法调用导致 Java 堆栈溢出