简体   繁体   English

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

[英]How do I avoid destructor stack overflow with deep nested data structure in C++?

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

The above program causes stack overflow under "release" build in Visual Studio 2010 IDE. 上面的程序在Visual Studio 2010 IDE中的“发行版”下导致堆栈溢出。

The question is: if you do need to create some data structure like the above, how to avoid this problem. 问题是:如果您确实需要创建类似上述的数据结构,如何避免此问题。

UPDATE: Now I have seen one meaningful answer. 更新:现在我看到了一个有意义的答案。 However this is not good enough. 但是,这还不够好。 Please consider I have updated MyClass to contain two (or more) shared_ptr s, and each of them can be an instance of MyClass or some other data. 请考虑我已经将MyClass更新为包含两个(或多个) shared_ptr ,并且每个都可以是MyClass的实例或其他一些数据。

UPDATE: Somebody updated the title for me and saying "deep ref-counted data structure", which is not necessary related to this question. 更新:有人为我更新了标题,并说“深度引用计数的数据结构”,与该问题无关。 Actually, shared_ptr is only a convenient example; 实际上, shared_ptr只是一个方便的示例。 you can easily change to other data types with the same problem. 您可以轻松地将其他数据类型更改为同样的问题。 I also removed the C++11 tag because it is not C++11 only problem as well. 我也删除了C++11标记,因为它也不是C ++ 11唯一的问题。

  • Make the stack explicit (ie put it in a container on the heap). 使堆栈显式(即,将其放入堆中的容器中)。
  • Have non-opaque pointers (non-void) so that you can walk your structure. 具有非透明指针(非void),以便您可以遍历结构。
  • Un-nest your deep recursive structure onto the heap container, making the structure non-recursive (by disconnecting it as you go along). 将深层递归结构取消嵌套到堆容器中,使该结构成为非递归结构(通过在进行过程中断开连接)。
  • Deallocate everything by iterating over the pointers collected above. 通过遍历上面收集的指针来释放所有内容。

Something like this, with the type of p changed so we can inspect it. 像这样,改变了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
}

Some final tips: 最后一些提示:

  • If you want to untangle any structure, your types should provide an interface that returns and releases all internal shared_ptr's, ie something like: std::vector<shared_ptr<MyClass>> yieldSharedPtrs() , perhaps within a ISharedContainer interface or something if you can't restrict yourself to MyClass. 如果您想解开任何结构,则您的类型应该提供一个返回并释放所有内部shared_ptr的接口,例如: std::vector<shared_ptr<MyClass>> yieldSharedPtrs() ,可能在ISharedContainer接口内,或者如果可以的话不要局限于MyClass。
  • For recursive structures, you should check that you don't add the same object to your ptr-container twice. 对于递归结构,应检查是否没有两次将同一对象添加到ptr-container中。

If you really have to work with such an odd code, you can increase the size of your stack. 如果确实需要使用这样的奇数代码,则可以增加堆栈的大小。 You should find this option in the project properties of Visual Studio. 您应该在Visual Studio的项目属性中找到此选项。

As already suggested, I must tell you that this kind of code should be avoided when working with a large mole of data structures, and increasing the stack size is not a good solution if you plan to release your software. 正如已经建议的那样,我必须告诉您,在使用大量数据结构时应避免使用此类代码,如果计划发布软件,则增加堆栈大小不是一个好的解决方案。 It may also terribly slow down your own computer if you abuse this feature, obviously. 显然,如果您滥用此功能,它也可能会极大降低计算机的速度。

Thanks to @Macke's tips, I have an improved solution like the following: 感谢@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 is a macro that defines a variable (param 2) as specified type (param 1) with thread local storage type, which means there is no more than one instance for each running thread. DEFINE_THREAD_LOCAL是一个宏,用于将变量(参数2)定义为具有线程本地存储类型的指定类型(参数1),这意味着每个正在运行的线程DEFINE_THREAD_LOCAL有一个实例。 Because thread_local keyword is still not available for mainstream compilers, I have to assume such a macro to make it work for compilers. 因为thread_local关键字仍不适用于主流编译器,所以我必须假定这样的宏才能使其适用于编译器。

For single thread programs, DEFINE_THREAD_LOCAL(type, var) is simply 对于单线程程序, DEFINE_THREAD_LOCAL(type, var)很简单

static type var;

The benefit of this solution is it do not require to change the class definition. 此解决方案的好处是不需要更改类定义。

Unlike @Macke's solution, I use std::queue rather than std::stack in order to keep the destruction order. 与@Macke的解决方案不同,我使用std::queue而不是std::stack来保持销毁顺序。

In the given test case, q.size() will never be more than 1. However, it is just because this algorithm is breadth-first. 在给定的测试用例中, q.size()永远不会大于1。但是,这仅仅是因为该算法是广度优先的。 If MyClass has more links to another instance of MyClass , q.size() will reach greater values. 如果MyClass具有指向MyClass另一个实例的更多链接,则q.size()将达到更大的值。

NOTE: It is important to remember use std::move to pass p to the queue. 注意:重要的是要记住使用std :: move将p传递到队列。 You have not solved the problem if you forgotten to do so, you are just creating and destroying a new copy of p, and after the visible code the destruction will still be recursive. 如果您忘记这样做,您还没有解决问题,您只是在创建和销毁p的新副本,并且在可见代码之后,销毁仍然是递归的。

UPDATE: the original posted code has a problem: q is going to be modified within pop() call. 更新:原始发布的代码有问题: q将在pop()调用中进行修改。 The solution is cache the value of q.front() for later destruction. 解决方案是缓存q.front()的值以供以后销毁。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM