【问题标题】:OOP interface when using indices for links使用链接索引时的 OOP 接口
【发布时间】:2019-03-27 23:33:42
【问题描述】:

TL;DR:我有一个链接的数据结构,我决定不使用指针,而是使用容器中的索引来表达这些链接。我是否可以将单个元素建模为独立对象以提高代码的可读性,而不会产生保持对数组的多个引用的成本?


假设我有一个链接的数据结构。为简单起见,我们以双向链表为例,带有删除节点的操作。对此进行建模的经典方法是使用指针:

struct Node {
  Node *prev, *next;
  void remove() { next->prev = prev; prev->next = next; }
};

但是指针有很多缺点。它们可能会浪费空间,因为通常无法选择指针大小来匹配用例。它们造成了较差的有线格式。如果我将节点保留在向量中,则调整大小可能会使指针无效。复制数据结构变得更加困难。所以我可以将索引放入某个数组中:

struct Node {
  int prev, next;
};
struct LinkedList {
  std::vector<Node> nodes;
  void remove(int i) {
    Node& n = nodes[i];
    nodes[n.next].prev = n.prev;
    nodes[n.prev].next = n.next;
  }
};

但是现在以前是单个节点的方法的操作变成了容器类的方法。这种语义转变使一些代码更难阅读。为了避免这个问题,我可以根据容器和节点索引对来进行表示。

struct Node { int prev, next; };
struct LinkedList;
struct NodeRef {
  int i;
  LinkedList& l;  // This reference here is what's worrying me
  NodeRef(int index, LinkedList& list) : i(index), l(list) {}
  NodeRef prev() const;
  NodeRef next() const;
  void remove();
};
struct LinkedList {
  std::vector<Node> nodes;
  NodeRef root() { return NodeRef(0, *this); }
};
NodeRef NodeRef::prev() const { return NodeRef(l.nodes[i].prev, l); }
NodeRef NodeRef::next() const { return NodeRef(l.nodes[i].next, l); }
void NodeRef::remove() {
  Node& n = l.nodes[i];
  l.nodes[n.next].prev = n.prev;
  l.nodes[n.prev].next = n.next;
}

所以现在我可以使用 NodeRef 以一种面向对象的方式来表达我的算法,将节点作为我可以迭代的实体,同时在幕后使用索引而不是指针。

但是当我有一些复杂的算法同时操作多个节点时,我会有多个 NodeRef 对象都引用同一个底层 LinkedList 对象。就内存消耗和复制它们的工作而言,这感觉有点浪费。我猜测编译器可能能够检测到一些冗余并摆脱它。但是我能做些什么来帮助它,以确保即使在语义上我有多个引用,它也会被优化为只使用一个引用?

【问题讨论】:

  • 您是否考虑过使用operator[] 重载来抽象出交互和连接的智能指针?智能指针也可以处理多个 NodeRef 对象的问题。
  • @Tzalumen:当没有明确的所有权时,智能指针天生就很棘手,大多数不是树的链接数据结构就是这种情况。智能指针的主要好处是不错的基于 RAII 的清理,但清理甚至不是我列出的指针的问题之一,所以我列出的所有问题仍然适用于智能指针。不知道你会如何在这里使用operator[]。在LinkedList 上定义它以构造NodeRef 可以使抽象代码更易于阅读,但不会影响手头的问题,因为冗余引用仍然存在。
  • 你看过std::weak_ptrstd::shared_from_this吗? weak_ptr 用于循环中的最终指针,shared_from_this 为您提供了一种简单的方法来通过多个 NodeRef 引用相同的 LinkedList 来强制共享所有权。
  • 另外一个问题,您是否有理由不只是使用std::list 而不是自己滚动?

标签: c++ oop data-structures idioms


【解决方案1】:

当您想要对 NodeRef 执行操作时,您可以要求提供 LinkedList,然后使用一个访问对象或 lambda 来存储您的单个引用并包装对 NodeRef 的调用。

例如,要从列表中删除节点,您可以使用:

struct NodeRef
{
  int i;
  // LinkedList& l;  // Remove this
  NodeRef(int index) : i(index) {}
  NodeRef prev(LinkedList& l) const;
  NodeRef next(LinkedList& l) const;
  void remove(LinkedList& l);
};
// Require that a list is supplied instead of storing a ref
void NodeRef::remove(LinkedList& l)
{
  Node& n = l.nodes[i];
  l.nodes[n.next].prev = n.prev;
  l.nodes[n.prev].next = n.next;
}

// create a lambda with a captured list that wraps the call
LinkedList linkedList;
//...
auto remover = [&linkedList](NodeRef node)
{
  node.remove(linkedList);
};
//...
NodeRef nodeRef(0);
remover(nodeRef);

不确定这是否是您想要的,但它避免了对列表的多次引用,并且可以允许重用 NodeRef 对象。

【讨论】:

    【解决方案2】:

    另一种选择是使用模板来存储指向列表的静态指针,在所有派生类之间共享。使用不同的 int 来区分您的列表和 NodeRef's。您不得不在编译时再次管理这些整数,这可能并不理想,但它确实解决了多引用问题。

    #include <vector>
    
    template <int X>
    struct LinkedList;
    
    template <int X>
    struct NodeRef
    {
      static LinkedList<X>* l;
      int i;
      NodeRef(int index) : i(index) {}
      NodeRef prev() const;
      NodeRef next() const;
      void remove();
    };
    
    struct Node { int prev, next; };
    
    template <int X>
    struct LinkedList 
    {
      LinkedList() 
      {
          NodeRef<X>::l = this;
      }
      std::vector<Node> nodes;
      NodeRef<X> root() { return NodeRef<X>(0); }
    };
    
    
    // Use the static pointer
    template<int X>
    void NodeRef<X>::remove()
    {
      auto& l = *NodeRef<X>::l;
      Node& n = l.nodes[i];
      l.nodes[n.next].prev = n.prev;
      l.nodes[n.prev].next = n.next;
    }
    
    
    
    int main()
    {
        LinkedList<0> linkedList;
    
        // All node refs templated by 0 share the list pointer
        auto nodeRef = linkedList.root();
    }
    

    【讨论】:

      【解决方案3】:

      您可以让所有LinkedLists 共享一个Nodes 池:

      struct LinkedList {
        static std::vector<Node> nodes; // <-- static
        void remove(int i) {
          Node& n = nodes[i];
          nodes[n.next].prev = n.prev;
          nodes[n.prev].next = n.next;
        }
      };
      

      这样,对于NodeRef,仅索引就足以识别引用的Node

      但是请注意,根据创建、修改和销毁LinkedLists 和Nodes 的频率,您可能会因为缓存未命中而在此处遇到性能问题,因为作为列表中的邻居的Nodes 可能在记忆中根本不是邻居。 您当前的方法可能已经存在此问题。在这种情况下,全局 Node 池会使情况变得更糟。

      此外,如果您有一个多线程应用程序,您将不得不执行通常的锁定机制,这会进一步降低您的应用程序的速度。

      【讨论】:

        猜你喜欢
        • 2019-04-11
        • 2020-07-12
        • 1970-01-01
        • 2014-09-10
        • 2015-10-12
        • 1970-01-01
        • 2023-01-10
        • 1970-01-01
        • 2013-02-08
        相关资源
        最近更新 更多