【问题标题】:How should ownership for two closely tied data structures be designed?应该如何设计两个紧密联系的数据结构的所有权?
【发布时间】:2014-03-16 13:39:44
【问题描述】:

我有一个类A 和一个结构B(它是一个普通的旧数据结构)。这两者以A在某种意义上代表整个系统的状态的方式对系统进行建模,并且由多个Bs和其他状态组成。这意味着如果没有A,B 可能毫无价值。我想创建一个 API,您可以在其中“添加”Bs 到 A,然后直接修改 Bs 的状态(通过保留对它们的引用之类的东西)。

不过,我不确定如何根据所有权进行设计。我有几个选择。这很复杂,因为我使用的是 C++。如果我使用 Java,我可能会这样设计代码:

class A {
    private List<B> bs = new ArrayList<>();

    public void add(B b) {
        bs.add(b);
    }
}

API 的用户自己创建Bs 的地方,或者像这样:

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,Bs 什么都不是。我什至可以以某种方式使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 被调整大小时,它可能会在内存中移动,从而使返回的引用无效。你将如何解决这个问题?或者你会尝试以什么其他方式来设计这个?

【问题讨论】:

  • 首先,不要取引用,存储指针。使所有权和生命周期明确。谁有权访问B?他们如何命名它?这种访问持续多长时间? B 能比它的 A 活得更久吗? B 是只读数据,还是必须处于活动状态?你能让B 不可变吗?您拥有完整的选项菜单,所提供的信息不会缩小到一个答案。给出一个不那么抽象的描述和你的抽象描述一样,你可能不知道抽象哪些细节是安全的。
  • 这是一个刚体物理模拟。 A 是一个世界,其中存储了它们之间的身体和关节。 B 是一个刚体,包含身体的形状、当前位置和速度等信息。

标签: c++ oop pointers reference ownership


【解决方案1】:

没有理由不能在 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 处理内存管理的方式。只要 someone 持有对 B 的特定实例的引用(通过 shared_ptr),该特定实例将保持驻留在内存中。 shared_ptr 与 Java 的不同之处在于只要引用计数减为 0,实例就会被删除。 Java 等待将来的某个时间点进行垃圾收集。这种模式在 C++ 中称为 RAII(资源获取是初始化)。

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

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 的建议隐藏复制构造函数和赋值运算符。

【讨论】:

  • 所以你推荐我的第二个模型shared_ptr?
  • 任一。我个人更喜欢第一个模型,因为您将B 的构造函数与A 的add/create 方法解耦。如果您将新的构造函数参数添加到B,您还需要将它们添加到第二个方法的create 方法中。话虽如此,它们都是有效的。使用shared_ptr 会让您的生活更轻松。
  • 当然,但是Bs 不应该存在于其中之一,并且只有一个A。
  • 第一个需要move 和make_shared。如果您不公开shared_ptr,第二个应该存储std::vector 的unique_ptr:手动delete 是错误的标志。例如,您未能阻止 A 中的复制构造/分配,如果您有 unique_ptrs,则会自动发生这种情况。
  • @Yakk 我更喜欢不使用 c++11,因为目前并非所有系统上的所有编译器都支持它(我进行科学计算并且必须在我的计算集群上编译,它经常运行一些令人讨厌的带有过时编译器的旧版 Linux)。
猜你喜欢
  • 2011-10-07
  • 2013-07-26
  • 2011-10-31
  • 2021-08-26
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2023-03-17
  • 2016-03-03
相关资源
最近更新 更多