【问题标题】:c++ creating cyclically dependent objects with raw pointersc ++使用原始指针创建循环依赖对象
【发布时间】:2020-09-04 16:12:11
【问题描述】:

我正在尝试创建一个连通图并对其执行某些计算。为此,我需要从该图中的每个节点访问其邻居,并从其邻居访问其邻居的邻居,依此类推。这不可避免地会产生许多(有用的)循环依赖。

下面是一个简化示例,其中包含 3 个相互连接的节点(例如三角形的 3 个顶点),我不确定这种方法是否是一种好方法,特别是如果清理留下任何内存泄漏:

#include <iostream>
#include <vector>

class A {
public:
    int id;
    std::vector<A*> partners;
 
    A(const int &i) : id(i) {
        std::cout << id << " created\n";
    }
    ~A() {
        std::cout << id << " destroyed\n";
    }
};

bool partnerUp(A *a1, A *a2) {
    if (!a1 || !a2)
        return false;

    a1->partners.push_back(a2);
    a2->partners.push_back(a1);

    std::cout << a1->id << " is now partnered with " << a2->id << "\n";

    return true;
}

int main() {
    std::vector<A*> vecA;
    vecA.push_back(new A(10));
    vecA.push_back(new A(20));
    vecA.push_back(new A(30));
 
    partnerUp(vecA[0], vecA[1]);
    partnerUp(vecA[0], vecA[2]);
    partnerUp(vecA[1], vecA[2]);

    for (auto& a : vecA) {
        delete a;
        a = nullptr;
    }
    vecA.clear();
 
    return 0;
}

我也知道我可以使用 shared_ptr + weak_ptr 来完成任务,但是智能指针会带来开销,我希望尽可能避免这种情况(我也讨厌使用 .lock( )一直访问数据,但这并不重要)。我使用智能指针重写代码如下,我想知道两段代码之间有什么区别(两段代码的输出相同)。

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

using namespace std;

class A {
public:
    int id;
    vector<weak_ptr<A>> partners;
 
    A(const int &i) : id(i) {
        cout << id << " created\n";
    }
    ~A() {
        cout << id << " destroyed\n";
    }
};

bool partnerUp(shared_ptr<A> a1, shared_ptr<A> a2) {
    if (!a1 || !a2)
        return false;

    a1->partners.push_back(a2);
    a2->partners.push_back(a1);

    cout << a1->id << " is now partnered with " << a2->id << "\n";

    return true;
}

int main() {
    vector<shared_ptr<A>> vecA;
    vecA.push_back(make_shared<A>(10));
    vecA.push_back(make_shared<A>(20));
    vecA.push_back(make_shared<A>(30));
 
    partnerUp(vecA[0], vecA[1]);
    partnerUp(vecA[0], vecA[2]);
    partnerUp(vecA[1], vecA[2]);

    return 0;
}

【问题讨论】:

  • “我想知道这两段代码有什么区别” 嗯,我不太明白你在找什么样的答案.
  • 使用vector&lt;unique_ptr&lt;A&gt;&gt; 或deque&lt;A&gt; 也很好(在main 中)。通常,人们想避开这个容器,只在图中存储指针。然后,删除就成了问题。
  • 第二个带有智能指针的代码应该没有泄漏,我想知道第一个代码是否也是如此。如果两个代码都没有泄漏,我更喜欢使用第一个。
  • 但是通过在 main() 中使用 vector> 我将无法执行 partnerUp 并且所有连接都丢失了
  • But by using vector&lt;unique_ptr&lt;A&gt;&gt; in main() I wouldn't be able to do partnerUp and all connections are lost 这不是真的。智能指针和原始指针的组合是一种有效的方法。如果你有一个树结构,一个节点唯一地保存它的子节点并引用它的父节点,对子节点使用unique_ptr,对父节点使用原始指针就可以了。对于复杂图,图本身将拥有节点并管理它们的连接,并且节点已经连接,在这里您也可以使用unqiue_ptr 和原始指针。

标签: c++ smart-pointers cyclic-dependency raw-pointer


【解决方案1】:

您可以通过使用所有权原则来防止内存泄漏:在每一点上,都需要一个负责释放内存的所有者。

在第一个示例中,所有者是 main 函数:它撤消所有分配。

在第二个示例中,每个图节点都有共享所有权。 vecA 和链接节点共享所有权。从某种意义上说,他们都有责任,如有必要,他们都可以免费拨打电话。

所以从这个意义上说,两个版本都有相对明确的所有权。第一个版本甚至使用了更简单的模型。但是:第一个版本在异常安全方面存在一些问题。这些在这个小程序中是不相关的,但是一旦将这段代码嵌入到更大的应用程序中,它们就会变得相关。

问题来自所有权转移:您通过new A 执行分配。这并没有明确说明所有者是谁。然后我们将它存储到向量中。但是向量本身不会在其元素上调用 delete;它只是调用析构函数(指针无操作)并删除自己的分配(动态数组/缓冲区)。 main 函数是所有者,它只在某个时刻释放分配,在循环结束时。如果 main 函数提前退出,例如由于异常,它将不会履行其作为分配所有者的职责 - 它不会释放内存。

这就是智能指针发挥作用的地方:它们清楚地说明所有者是谁,并使用 RAII 来防止异常问题:

class A {
public:
    int id;
    vector<A*> partners;
 
    // ...
};

bool partnerUp(A* a1, A* a2) {
    // ...
}

int main() {
    vector<unique_ptr<A>> vecA;
    vecA.push_back(make_unique<A>(10));
    vecA.push_back(make_unique<A>(20));
    vecA.push_back(make_unique<A>(30));
 
    partnerUp(vecA[0].get(), vecA[1].get());
    partnerUp(vecA[0].get(), vecA[2].get());
    partnerUp(vecA[1].get(), vecA[2].get());

    return 0;
}

该图仍然可以使用原始指针,因为所有权现在完全由 unique_ptr 负责,而这些指针归 vecA 所有,并且归 main 所有。 Main 退出,销毁 vecA,这会销毁它的每个元素,而这些元素会销毁图形节点。

不过,这仍然不理想,因为我们使用了一种不必要的间接方式。我们需要保持图节点的地址稳定,因为它们是从其他图节点指向的。因此我们不应该在 main 中使用vector&lt;A&gt;:如果我们通过push_back 调整它的大小,这会改变其元素的地址——图节点——但我们可能已经将这些地址存储为图关系。也就是说,我们可以使用vector,但前提是我们没有创建任何链接。

即使在创建链接之后,我们也可以使用deque。 deque 在 push_back 期间保持元素的地址稳定。

class A {
public:
    int id;
    vector<A*> partners;

    // ...

    A(A const&) = delete; // never change the address, since it's important!

    // ...
};

bool partnerUp(A* a1, A* a2) {
    // ...
}

int main() {
    std::deque<A> vecA;
    vecA.emplace_back(10);
    vecA.emplace_back(20);
    vecA.emplace_back(30);
 
    partnerUp(&vecA[0], &vecA[1]);
    partnerUp(&vecA[0], &vecA[2]);
    partnerUp(&vecA[1], &vecA[2]);
 
    return 0;
}

在图中删除的实际问题是当您在 main 中没有像 vector 这样的数据结构时:可以只保留指向一个或多个节点的指针,您可以从这些节点到达所有其他节点主要的。在这种情况下,您需要图遍历算法来删除所有节点。这就是它变得更复杂,因此更容易出错的地方。

就所有权而言,这里的图本身将拥有其节点的所有权,而 main 仅拥有图的所有权。

int main() {
    A* root = new A(10);
 
    partnerUp(root, new A(20));
    partnerUp(root, new A(30));
    partnerUp(root.partners[0], root.partners[1]);

    // now, how to delete all nodes?
 
    return 0;
}

为什么会推荐第二种方法?

因为它遵循一种广泛而简单的模式,可以降低内存泄漏的可能性。如果您总是使用智能指针,那么总会有一个所有者。放弃所有权的错误是不可能的。

但是,使用共享指针,您可以形成多个元素保持活动状态的循环,因为它们在一个循环中相互拥有。例如。 A 拥有 B,B 拥有 A。

因此,典型的经验法则建议是:

  • 使用堆栈对象,或者如果不可能,使用unique_ptr,或者如果不可能,使用shared_ptr。
  • 对于多个元素,请按顺序使用container&lt;T&gt;、container&lt;unique_ptr&lt;T&gt;&gt; 或container&lt;shared_ptr&lt;T&gt;&gt;。

这些是经验法则。如果您有时间考虑一下,或者有一些要求,例如性能或内存消耗,那么定义自定义所有权模型是有意义的。但是,您还需要花时间确保安全并对其进行测试。所以它应该真的给你一个很大的好处,值得付出所有努力来确保它的安全。我建议不要假设 shared_ptr 太慢。这需要在应用程序的上下文中查看,并且通常是衡量的。获得正确的自定义所有权概念太棘手了。例如,在我上面的一个示例中,您需要非常小心地调整矢量的大小。

【讨论】:

  • A 类 - vector&lt;weak_ptr&lt;A&gt;&gt; partners; - 与使用 shared_ptr 相比,使用 weak_ptr 对合作伙伴有什么好处吗?
  • 另外——你在哪里声明Hence we cannot use vector&lt;A&gt; in main (if we resize that, it changes the addresses).——即使这些指针的存储位置发生了变化,原始指针肯定仍然有效吗?不过,双端队列会提供更好的性能。
  • @XiasuYang 具有动态大小的容器(如list、deque、vector,...)不会将它们的元素存储在堆栈上,而是在堆上。除此之外,如果涉及到无泄漏的观点,您不应该考虑堆栈和堆,而应该考虑存储持续时间和所有权。如果您有一个名为graph 的课程,您将有一个例如成员container&lt;…&gt; nodes、graph 对nodes 拥有唯一所有权,nodes 的存储持续时间由graph 的生命周期定义。 nodes 对其元素拥有唯一的所有权,它们的存储期限由 nodes 的生命周期决定。
  • @XiasuYang if container&lt;…&gt; nodes 是例如@ 987654361@,节点的所有权 - 从c ++运行时的角度来看 - 在指针而不是那些指针指向的节点上,只有指针(而不是元素)将被删除。如果是container&lt;node&gt; nodes,则所有权在node 元素上。如果是container&lt;std::unique_ptr&lt;node&gt;&gt; nodes,则拥有unique_ptr 实例的所有权,而unique_ptr 拥有node 的所有权。
  • @Den-Jason 该图实际上是用于流体动力学计算的多边形网格,因此没有“发现”多边形,所有多边形必须在网格的生命周期内可用,并在获得收敛解决方案后删除.我意识到完全没有必要使用 shared_ptr 和 unique_ptr 在没有 refCount 的情况下完成这项工作,但是通过使用 deque,我确实可以完全摆脱智能指针,这是我的初衷(我的问题中的第一个代码)。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2012-05-23
  • 2011-05-12
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2013-10-09
  • 1970-01-01
相关资源
最近更新 更多