这是 std::vector 和 std::shared_ptr 内存泄漏的错误吗?

Posted

技术标签:

【中文标题】这是 std::vector 和 std::shared_ptr 内存泄漏的错误吗?【英文标题】:Is this std::vector and std::shared_ptr memory leakage a bug? 【发布时间】:2019-01-30 10:02:06 【问题描述】:

假设这个类Foo

struct Foo 
    std::shared_ptr<int> data;
    std::shared_ptr<std::vector<Foo>> foos;
;

它有一个指向 int 的指针

它有一个指向该程序中将存在的所有实例的指针(因此这些实例之一 == *this

让我们创建一个Foo 的实例,并在将一些实例添加到.foos 后查看其.data 成员变量的use_count()

int main() 
    Foo foo;
    foo.data = std::make_shared<int>(5);
    foo.foos = std::make_shared<std::vector<Foo>>();
    foo.foos->resize(8);

    for (auto & f : *foo.foos) 
        f.data = foo.data;
        f.foos = foo.foos;
    
    std::cout << "use count: " << foo.data.use_count() << '\n';    

输出:

use count: 9

这很好 (1 foo + 8 .foos)。 不过好像main()返回时,还是会有 9 8个指针指向.data!这可以通过将foo 放入本地范围并让一个额外的指针指向.data 来随后观察这个指针use_count() 来证明:

int main() 
    std::shared_ptr<int> ptr;
    std::cout << "use count | before: " << ptr.use_count() << '\n';

     //begin scope
        Foo foo;
        foo.data = std::make_shared<int>(5);
        foo.foos = std::make_shared<std::vector<Foo>>();
        foo.foos->resize(8);

        for (auto & f : *foo.foos) 
            f.data = foo.data;
            f.foos = foo.foos;
        
        ptr = foo.data;
        std::cout << "use count | inside: " << ptr.use_count() << '\n';

     //end scope

    std::cout << "use count | after: " << ptr.use_count() << '\n';

输出是:

use count | before: 0
use count | inside: 10
use count | after: 9

这不好。我希望use count | after 成为1,因为foo 并且它的所有成员都应该在范围结束时解构。好吧,foo 确实被解构了(否则 use_count | after 将是 10 而不是 9)但它的 .foos 向量指针没有被解构。而ptr 只是一个std::shared_ptr&lt;int&gt;,因此与struct Foo 毫无关系。所有这些都可以通过提供 struct Foo 一个析构函数来解决,该析构函数 reset()s 手动指向 .foos-&gt;data 指针:

#include <memory>
#include <iostream>
#include <vector>

struct Foo 
    ~Foo() 
        for (auto& p : *foos) 
            p.data.reset();
        
    

    std::shared_ptr<int> data;
    std::shared_ptr<std::vector<Foo>> foos;
;

int main() 
    std::shared_ptr<int> ptr;
    std::cout << "use count | before: " << ptr.use_count() << '\n';

    
        Foo foo;
        foo.data = std::make_shared<int>(5);
        foo.foos = std::make_shared<std::vector<Foo>>();
        foo.foos->resize(8);

        for (auto & f : *foo.foos) 
            f.data = foo.data;
            f.foos = foo.foos;
        
        ptr = foo.data;
        std::cout << "use count | inside: " << ptr.use_count() << '\n';
    

    std::cout << "use count | after: " << ptr.use_count() << '\n';

产生更好的输出:

use count | before: 0
use count | inside: 10
use count | after: 1

但是必须手动重置这些指针似乎很奇怪。为什么std::vectorstd::shared_ptr 不在这里自动执行此操作?它是一个错误吗?


我正在使用 Visual Studio Community 2017 版本 15.9.5 - 感谢您的帮助!

【问题讨论】:

不明白,在ent ptr 处仍然指向foos 数据并且使用count 为1?那么……有什么问题? 我明白了……对不起…… 当程序超出 main() 范围时,指针将被释放。 没有错误,这是预期的行为。您必须手动重置那些嵌套的Foos,因为您正在创建循环引用(向量中的Foos 对其所在的向量有一个 shared_ptr,从而使它们保持活动状态)并且 shared_ptr 并非旨在处理它。 离题:解构 == 销毁... 【参考方案1】:

问题是你有一个循环引用。

foo 被销毁时,它会减少其shared_ptr引用计数,但不会达到零。

所以即使 std::shared_ptr&lt;std::vector&lt;Foo&gt;&gt; 是 "inaccessible" ,它上面仍然有指针。 (注意:垃圾收集器使用“可访问性”来收集/释放指针)。

打破循环的常用方法是使用std::weak_ptr

【讨论】:

也许要进一步澄清:foo.foos 的每个条目都是一个Foo,它保留对foo.datafoo.foos 的引用。此时所有指向foo.foos 的指针都可以互换,删除一个不会影响其余的。 谢谢,这看起来像我正在寻找的东西。尽管我还不知道如何在此示例代码中使用weak_ptr,但我接受了答案。它似乎没有 -&gt; 这样的运算符。 在使用关联的std::shared_ptr 之前,您必须先lock 它(检查nullptr)。 好的,我明白了。但是由于weak_ptr 想要设置为现有的shared_ptr,我将不得不将shared_ptr&lt;vector&lt;Foo&gt;&gt; weak_ptr&lt;vector&lt;Foo&gt;&gt; 都放在结构中,对吗? 您不必两者都放。很难说正确的方法,因为您的样本被简化了。 std::vector&lt;std::weak_ptr&lt;Foo&gt;&gt; 似乎是一种选择。【参考方案2】:

您创建了一个循环依赖项:每个Foo 包含所有其他Foos 的shared_ptrs(即共享所有权)。这意味着Foo 永远不会被破坏:要被破坏,use_count 必须为零。但在进入析构函数之前它不能为零,因为其他每个 Foo 仍然持有一个引用。

这是共享所有权限制的经典案例 - 与某些信念相反,它不会自动解决您的所有所有权问题。

我也会质疑每个Foo 存储指向所有Foos 的相同指针的意义。如果这就是你想要做的,它应该只是static,但这听起来也不是好的设计。也许您可以详细说明您想要解决的实际问题(在一个新问题中)?

【讨论】:

所有权根据定义不是循环的。如果您最终获得循环所有权,则会出现语义错误。

以上是关于这是 std::vector 和 std::shared_ptr 内存泄漏的错误吗?的主要内容,如果未能解决你的问题,请参考以下文章

从原始数据就地创建 std::vector

std::vector::resize() 与 std::vector::reserve()

使用 2D std::vector 对 SYCL 进行矩阵乘法

std::vector push_back 是瓶颈

从 C++ std::vector 中删除元素

奇怪的 std::vector::reverse_iterator 和字符串行为