应该如何设计两个紧密关联的数据结构的所有权

How should ownership for two closely tied data structures be designed?

本文关键字:两个 关联 数据结构 所有权      更新时间:2023-10-16

我有一个类A和一个结构B(这是一个普通的旧数据结构(。这两者以A的方式对系统进行建模,从某种意义上说,它代表了整个系统的状态,并且由多个B和其他状态组成。这意味着没有A B可以说一文不值。我想制作一个 API,您可以在其中将B"添加"到A,然后直接修改B的状态(通过保留对它们的引用之类的内容(。

不过,我不确定的是,如何从所有权的角度设计它。我有几个选择。由于我正在使用C++,这很复杂。如果我使用Java,我可能会像这样设计代码:

class A {
    private List<B> bs = new ArrayList<>();
    public void add(B b) {
        bs.add(b);
    }
}

API 的用户自行创建B,或如下所示:

class A {
    private List<B> bs = new ArrayList<>();
    public B create(int data) {
        B b = new B(data);
        bs.add(b);
        return b;
    }
}

A系统自行创建对象的位置。这可能是更好的方法,因为没有A B就什么都不是。我甚至可以以某种方式使B构造函数仅供A访问,但我不确定如何。

但请记住,这是在Java中。该系统将在C++中编程,并且它的工作方式不同。

我想第一个示例看起来很相似:

class A {
public:
    void add(B& b)
    {
        bs.push_back(&b);
    }
private:
    std::vector<B*> bs;
};

但由于我更喜欢第二个选项,我尝试翻译它:

class A {
public:
    B& create(int data)
    {
        bs.emplace_back(data);
        return bs.back();
    }
private:
    std::vector<B> bs;
};

但在这种情况下,我返回对B的引用.调整bs大小时,它可能会在内存中移动,从而使返回的引用无效。你会如何解决这个问题?或者你会尝试以什么其他方式设计它?

没有

理由不能在C++中使用任一数据模型,只是语法不同。

在第一种情况下,add 的调用方必须实例化 B 的实例。 然后它将 B 传递到 A 的实例中,然后 持有指向 b 的引用(或指针,C++术语(:

#include <memory>
#include <vector>
class B {
};
class A {
public:
    void add(std::shared_ptr<B> b) {
        bs.push_back(b);
    }
private:
    std::vector<std::shared_ptr<B> > bs;
};
int main(int argc, const char *argv[]) {
    A a;
    std::shared_ptr<B> b(new B);
    a.add(b);
}

在此实现中,我们持有指向 B 的每个实例的共享指针。 共享指针使用类似于 Java 处理内存管理的引用计数器。 只要有人持有对 B 的特定实例的引用(通过 shared_ptr(,该特定实例将保留在内存中。 shared_ptr 与 Java 的不同之处在于,一旦引用计数为 0,实例就会被删除。 Java等待将来的某个时间点进行垃圾回收。 此模式在 C++ 中称为 RAII(资源获取是初始化(。

您的第二个模型可以类似地处理。 您可以在向量中使用 shared_ptr s,也可以直接使用原始指针。 由于我已经在上面展示了shared_ptr,我将在这里显示原始指针:

class B {};
class A {
public:
    ~A() {
        for (std::vector<B*>::iterator it = bs.begin(); it != bs.end(); ++it) {
            delete *it;
        }
        bs.clear();
    }
    B* create(int data) {
        B *b = new B(data);
        bs.push_back(b);
        return b;
    }
private:
    // block copy construction since we don't manually copy the contents of our
    // vector
    A(const A&) {}
    void operator=(const A&) {}
    std::vector<B*> bs;
};

在这个设计中,由于我们使用了原始指针,我们必须自己管理内存。 这就引入了对析构函数的需求,该析构函数在销毁A实例时释放存储在向量中的所有B实例。 我们现在还返回一个指针,指向我们在 create 中创建的每个新B实例。 这种内存模型比第一种方法更加模糊,因为我们有原始指针,因此需要采用一个策略,该策略只能通过纪律强制执行,关于谁拥有并负责每个B实例的内存管理。 针对此 API 编码的开发人员可以删除返回给他的 B 实例,这将导致下次尝试访问该 B 实例时出现段错误或未定义的行为。

我的建议?使用shared_ptr,除非这需要快速出血性能并且每个操作都很重要。

编辑

根据 Yakk 的建议隐藏复制构造函数和赋值运算符。