如何在C ++中避免使用深层嵌套数据结构的析构函数堆栈溢出?

时间:2011-12-10 09:34:35

标签: c++ stack-overflow destructor

int count;

class MyClass {
    std::shared_ptr<void> p;
public:
    MyClass(std::shared_ptr<void> f):p(f){
        ++count;
    }
    ~MyClass(){
        --count;
    }
};

void test(int n){
    std::shared_ptr<void> p;
    for(int i=0;i<n;++i){
        p = std::make_shared<MyClass>(p);
    }
    std::cout<<count<<std::endl;
}

int main(int argc, char* argv[])
{
    test(200000);
    std::cout<<count<<std::endl;
    return 0;
}

上述程序在Visual Studio 2010 IDE中的“release”版本下导致堆栈溢出。

问题是:如果你确实需要像上面那样创建一些数据结构,那么如何避免这个问题。

更新:现在我看到了一个有意义的答案。但这还不够好。请注意我已将MyClass更新为包含两个(或更多)shared_ptr个,其中每个都可以是MyClass的实例或其他一些数据。

更新:有人为我更新了标题并说“深度重新计算的数据结构”,这与此问题无关。实际上,shared_ptr只是一个方便的例子;您可以轻松地更改为具有相同问题的其他数据类型。我还删除了C++11标记,因为它也不仅仅是C ++ 11的问题。

3 个答案:

答案 0 :(得分:2)

  • 使堆栈显式化(即将其放在堆上的容器中)。
  • 拥有非透明指针(非虚空),以便您可以走你的结构。
  • 将深度递归结构取消嵌套到堆容器中,使结构不递归(通过在进行时断开连接)。
  • 通过迭代上面收集的指针来释放所有内容。

像这样的东西,改变了p的类型,所以我们可以检查它。

std::shared_ptr<MyClass> p;

~MyClass() {
    std::stack<std::shared_ptr<MyClass>> ptrs;
    std::shared_ptr<MyClass> current = p;

    while(current) {
        ptrs.push_back(current);
        current = current->p;
        ptrs.back()->p.reset(); // does not call the dtor, since we have a copy in current
    }

    --count;
    // ptrs dtor deallocates every ptr here, and there's no recursion since the objects p member is null, and each object is destroyed by an iterative for-loop
}

最后一些提示:

  • 如果你想解开任何结构,你的类型应该提供一个返回和释放所有内部shared_ptr的接口,例如:std::vector<shared_ptr<MyClass>> yieldSharedPtrs(),可能在ISharedContainer接口中,或者如果你不能限制自己MyClass的。
  • 对于递归结构,您应该检查是否没有将同一个对象添加到ptr容器两次。

答案 1 :(得分:0)

如果你真的需要处理这么奇怪的代码,你可以增加堆栈的大小。您应该在Visual Studio的项目属性中找到此选项。

正如已经建议的那样,我必须告诉你,在处理大量数据结构时应避免使用这种代码,如果计划发布软件,增加堆栈大小并不是一个好的解决方案。如果您滥用此功能,它可能也会使您自己的计算机速度变慢。

答案 2 :(得分:0)

感谢@ Macke的提示,我有一个改进的解决方案,如下所示:

~MyClass(){
   DEFINE_THREAD_LOCAL(std::queue< std::shared<void> >, q)
   bool reentrant = !q.empty();
   q.emplace(std::move(p)); //IMPORTANT!
   if(reentrant) return;

   while(!q.empty()){
       auto pv = q.front();
       q.pop();
   }
}

DEFINE_THREAD_LOCAL是一个宏,它将变量(param 2)定义为具有线程本地存储类型的指定类型(param 1),这意味着每个正在运行的线程只有一个实例。由于thread_local关键字仍然不适用于主流编译器,因此我必须假设这样一个宏才能使其适用于编译器。

对于单线程程序,DEFINE_THREAD_LOCAL(type, var)只是

static type var;

此解决方案的好处是不需要更改类定义。

与@ Macke的解决方案不同,我使用std::queue而不是std::stack来保留销毁订单。

在给定的测试用例中,q.size()永远不会超过1.但是,这只是因为这个算法是广度优先的。如果MyClass包含指向MyClass其他实例的更多链接,则q.size()将达到更高的值。

注意:记住使用std :: move将p传递给队列非常重要。如果忘记这样做,你还没有解决问题,你只是在创建和销毁p的新副本,在可见代码之后,破坏仍然是递归的。

更新:原始发布的代码存在问题:q将在pop()调用中进行修改。解决方案是缓存q.front()的值以便以后销毁。