C++ 在不使用弱指针的情况下解析智能指针循环引用

C++ 在不使用弱指针的情况下解析智能指针循环引用,c++,design-patterns,garbage-collection,smart-pointers,circular-reference,C++,Design Patterns,Garbage Collection,Smart Pointers,Circular Reference,假设我们有一个设计,其中一个对象集合可能对集合中的其他对象具有往复依赖性: struct Object { ... virtual void method(); private: std::vector<std::shared_ptr<Object>> siblings; }; struct对象 { ... 虚空法(); 私人: std::病媒兄弟姐妹; }; 允许出现循环引用(不代表退化情况)。通常,循环引用将由弱指针解析,但这需要所有权的分层概念,这不

假设我们有一个设计,其中一个对象集合可能对集合中的其他对象具有往复依赖性:

struct Object
{
  ...
  virtual void method();
private:
  std::vector<std::shared_ptr<Object>> siblings;
};
struct对象
{
...
虚空法();
私人:
std::病媒兄弟姐妹;
};
允许出现循环引用(不代表退化情况)。通常,循环引用将由弱指针解析,但这需要所有权的分层概念,这不适用于所有对象都是对等对象的场景

我们如何在不使用弱指针的情况下解决循环引用的问题?是否有这种设计模式和/或是否可以应用专门的垃圾收集库?(“专门化”的意思是,它不是一个保守的垃圾收集器,扫描整个内存空间中的根,例如Boehm GC,而是提供一个API,将操作范围仅限于感兴趣的对象,并提供在托管对象中显式注释/枚举根的方法。)

当然,我认为理想的解决方案是避免出现相互依赖的设计,但就当前问题而言,请处理相互依赖设计无法避免的约束。通过激励实例,考虑一个递归神经网络,其中每个神经元被表示为一个对象,该对象明确地存储对其连接神经元的引用。
我已经标记了这个问题
C++
,但也欢迎不懂语言的答案。

一个解决方案是每种类型都有一个成员,释放所有引用

struct type1
{
  std::shared_ptr<struct type2> ptr;
  void reset() { ptr.reset(); }
};
struct type2
{
  std::shared_ptr<type1> ptr;
  type2(std::shared_ptr<type1> & ptr) : ptr{ptr} {}
  void reset() { ptr.reset(); }
};
结构类型1
{
std::共享的ptr;
void reset(){ptr.reset();}
};
结构类型2
{
std::共享的ptr;
类型2(std::shared_ptr&ptr):ptr{ptr}{}
void reset(){ptr.reset();}
};
它不能像正确的RAII那样自动(因为需要一个额外的步骤,而不仅仅依赖析构函数),但只要你遵循合同,它就会保持对象的活动状态,只要它们需要,然后在以后释放它们。根据具体用途,可能还需要移动到两阶段初始化(例如,对于type1对象,创建对象,将其分配给共享的ptr,然后才创建type2对象)


虽然如果这是您的模式,您也可以经常移动到简单地让type2存储一个原始指针,而不必担心拥有对象的生命周期。对于这样的循环链,必须有一个外部引用,这是开始展开的适当位置。

在某些情况下,我们可以将
对象
实例作为一个组进行管理,
std::shared\u ptr
的别名构造函数提供了问题的部分解决方案。我不认为这是一个恰当的解决办法,但我希望能引起更多的讨论。我将在提出的人工神经网络用例的上下文中描述解决方案,而不是使用完全通用的公式化

问题 我们有一个
Neuron
类,其中每个实例以可能的往复关系引用其他神经元(即,预期会出现循环引用)

API允许客户端获取单个
Neuron
实例作为共享指针。当客户机持有这样一个指针时,如果
网络本身超出了范围,这应该无关紧要;所引用的
神经元的所有依赖项仍应保持活动状态:

std::shared_ptr<Neuron> neuron;
{
  auto network = Network::createNetwork();
  neuron = network.getNeuron(0);
}
neuron.inputs[0]; // <-- alive and well despite the
                  //     {network} smart pointer 
                  //     having been destructed.
分析 上述解决方案为我们提供了以下信息:

  • 对于单独持有的
    Neuron
    实例,客户端获得正常的
    std::shared_ptr
    语义
  • 客户不需要担心当他们的
    网络
    容器超出范围时会发生什么
  • 允许
    Neuron
    实例中的循环引用,并且不会干扰内存管理
但是,它有以下限制,这使得它充其量只能是部分解决方案,可能根本不是解决方案:

  • 需要由具有所有权语义的某个容器类执行管理
  • 不支持多个容器共同拥有对象(即,
    Neuron
    只能属于单个
    网络

仍然在寻找更好的答案,但同时,希望这可能是一些过客的好奇心。

谢谢你的解决方案。因此,当我们的
对象
类型只有一个可能的交互依赖项时,我们可以创建一个代理类型,它本质上是一对,持有指向对象及其依赖项的共享指针,从而确保只要这个代理永远不会被破坏,对象的依赖项就永远不会过期(我们可以使用RAII)。然而,据我所知,当对象具有可变数量的依赖项时,这种类型的解决方案不起作用。这和你的想法一致吗?事实上,没有。shared_ptr有一个相当奇怪的别名功能,可以让你说一个shared_ptr实际上操纵了一个shared_ptr的生命周期。这样就可以得到这样一个别名实例的向量。请注意,这不会帮助您解决获取实际对象(type1*)的问题,但允许您在一个位置收集对所有要保持活动状态的对象的引用。我当然可以想象创建一个容器,在其中聚合共享指针,以便它们各自的对象不会超出范围,但我无法想象我们如何以这样一种方式做到这一点,即保持自动RAII风格的内存管理。据我所知,我们需要定期遍历对象网络以查看哪些对象仍然可以访问,例如,滚动我们自己的垃圾收集器。
weak\u ptr
的设计可以防止对象在使用时被删除,因为您必须
class Network : public std::enable_shared_from_this<Network> {
  std::vector<Neuron> neurons;

public:
  static std::shared_ptr<Network> createNetwork();
  std::shared_ptr<Neuron> getNeuron(size_t indx);
};
std::shared_ptr<Neuron> neuron;
{
  auto network = Network::createNetwork();
  neuron = network.getNeuron(0);
}
neuron.inputs[0]; // <-- alive and well despite the
                  //     {network} smart pointer 
                  //     having been destructed.
std::shared_ptr<Neuron> Network::getNeuron(size_t const indx) {
  return std::shared_ptr<Neuron>(shared_from_this(), &neurons[indx]);
}