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

问题描述

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

struct Object
{
  ...
  virtual void method();
private:
  std::vector<std::shared_ptr<Object>> siblings;
};

允许出现循环引用(不代表退化的情况)。通常,循环引用可以通过弱指针来解决,但这需要所有权的层次结构概念,不适用于所有对象都是同等对象的情况。

如何在不使用弱指针的情况下解决循环引用的问题?是否为此提供设计模式和/或是否可以应用专门的垃圾收集库? (“专用”的含义不是保守的垃圾收集器,它会扫描整个内存空间中的根,例如Boehm GC,而是提供一种API,该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(); }
};

它不能像适当的RAII一样自动进行(因为需要额外的步骤,而不仅是依赖于析构函数),但是只要您遵循合同,它就会使对象保持活动状态,只要它们需要然后将它们释放。根据确切的用法,可能还需要进行两步初始化(例如,使用type1对象,创建该对象,将其分配给shared_ptr,然后再创建type2对象)。

尽管这是您的模式,但您也经常可以移动到只让type2存储原始指针,而不必担心所有拥有对象的生命周期。使用这样的环形链 somewhere 时,必须有一个外部参考,这才是开始展开的合适位置。

,

在某些情况下,我们可以将Object实例作为一个组来管理,std::shared_ptr的别名构造函数为该问题提供了部分解决方案。我认为这不是一个合适的解决方案,但我将其发布,希望引起更多讨论。我将在拟议的人工神经网络用例的背景下描述解决方案,而不是使用完全通用的公式化。

问题

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

struct Neuron {
  std::shared_ptr<Neuron> inputs,outputs;
};

我们希望自动管理Neuron实例的内存,以便只要我们持有指向Neuron的智能指针,就可以确保其所有依赖项都保持活动状态(即未过期)

部分解决方案

将神经元分组为网络是很自然的,因此我们可以引入Network类,它是一个管理并拥有Neuron实例集合的容器:

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);
};

API允许客户端获取单个Neuron实例作为共享指针。当客户端拥有这样的指针时,Network本身是否超出范围并不重要;引用的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.

为此,我们可以使用std::shared_ptr的别名构造函数:

std::shared_ptr<Neuron> Network::getNeuron(size_t const indx) {
  return std::shared_ptr<Neuron>(shared_from_this(),&neurons[indx]);
}

分析

以上解决方案为我们提供了以下内容:

  • 客户端对于单独持有的std::shared_ptr实例具有正常的Neuron语义。
  • 客户无需担心Network容器超出范围时会发生什么情况。
  • Neuron实例中允许使用循环引用,并且不会干扰内存管理。

但是,它具有以下局限性,因此充其量只能作为部分解决方案,甚至根本不是解决方案:

  • 要求管理必须由具有所有权语义的某些容器类执行。
  • 不支持多个容器共同拥有对象(即Neuron只能属于单个Network)。

仍在寻找更好的答案,但与此同时,希望这可能对某些过世的灵魂产生好奇。