如何实现一个避免未定义行为的侵入式链表?

时间:2015-12-07 13:35:49

标签: c++ c++11

几年来我第三次发现自己需要一个不允许提升的项目的侵入性链表(问管理......)。

我第三次发现侵入式链表实现我的工作完美,但我真的不喜欢它使用未定义的行为 - 即将指针转换为列表节点到指向包含该列表节点的对象。

这个糟糕的代码目前看起来像这样:

struct IntrusiveListNode {
    IntrusiveListNode * next_;
    IntrusiveListNode * prev_;
};

template <typename T, IntrusiveListNode T::*member>
class IntrusiveList {
// snip ...
private:
    T & nodeToItem_(IntrusiveListNode & node) {
        return *(T*)(((char*)&node)-((size_t)&(((T*)nullptr)->*member)));
    }

    IntrusiveListNode root_;
};

我真的不在乎nodeToItem_有多丑,但我想保持IntrusiveList的公共接口和语法相同。具体来说,我想使用IntrusiveList<Test, &Test::node_>而不是IntrusiveList<Test, offsetof(Test, node_)>来指定列表类型的类型。

几乎是2016年 - 有没有办法在不调用未定义的行为的情况下做到这一点?

编辑: 我想在这里总结一些建议的解决方案(涉及列表的不同结构):

  1. 生活在未定义的行为中,因为该语言似乎具有任意限制,可以防止反向使用成员指针。

  2. IntrusiveListNode中存储指向包含类的附加指针。这当前可能是最干净的解决方案(不需要更改接口),但在每个列表节点中都需要第三个指针(可能有小的优化)。

  3. IntrusiveListNode导出并使用static_cast。在boost中,这是侵入式链表的base_hook版本。我想坚持使用member_hook版本以避免引入多重继承。

  4. 存储指向下一个和上一个包含类的指针,而不是指向IntrusiveListNode中下一个和上一个列表节点的指针。这使得在侵入列表中创建根节点变得困难。列表必须包含T的完整实例化(这是不可能的,例如,如果T是抽象的),或者列表的末尾需要是空指针(这会破坏{{1}只允许前向迭代)。

  5. Boost侵入式列表有--list.end()版本可以某种方式工作,但实现尚未被理解(并且它可能还依赖于未定义的行为)。

  6. 问题仍然存在:是否有可能使用双向迭代支持,没有未定义的行为以及没有“不必要的”内存开销来创建基于成员的侵入式列表?

6 个答案:

答案 0 :(得分:9)

我会侧面解决问题并使用包含合适的node<T> 成员链接范围。处理双向,侵入性 list我会使用这样的非对称node<T>

template <typename T>
class intrusive::node
{
    template <typename S, node<S> S::*> friend class intrusive::list;
    template <typename S, node<S> S::*> friend class intrusive::iterator;

    T*       next;
    node<T>* prev;
public:
    node(): next(), prev() {}
    node(node const&) {}
    void operator=(node const&) {}
};

基本思路是list<T, L>包含node<T>使用 next指针指向第一个元素。那是公平的 直截了当:给p指针T指向下一个的链接 可以使用(p->*L).next遍历节点。但是,而不是 直接使用T*导航列表,实际上是iterator<T, L> 使用指向node<T>的指针:虽然这不是必需的 向前遍历,它允许向后遍历和插入 列表中没有特殊处理的列表中的任何位置。

复制构造函数和复制赋值被定义为什么都不做 复制节点时避免半插入节点。取决于 相反= delete可能更合理 这些行动。但是,这与手头的问题无关。

迭代器只使用指向node<T> next的指针 成员指向当前节点。对于第一个元素 list这是指向list<T, L>的{​​{1}}成员的指针。 假设您有一个指向合适的node<T>的指针node<T>可以从中创建:

iterator<T,
L>

取消引用只使用template <typename T, intrusive::node<T> T::*Link> class intrusive::iterator { template <typename S, node<S> S::*> friend class intrusive::list; node<T>* current; public: explicit iterator(node<T>* current): current(current) {} T& operator*() { return *this->operator->(); } T* operator->() { return this->current->next; } bool operator== (iterator const& other) const { return this->current == other.current; } bool operator!= (iterator const& other) const { return !(*this == other); } iterator& operator++() { this->current = &(this->current->next->*Link); return *this; } iterator operator++(int) { iterator rc(*this); this->operator++(); return rc; } iterator& operator--() { this->current = this->current->prev; return *this; } iterator operator--(int) { iterator rc(*this); this->operator--(); return rc; } }; 指针。同样如此 正向迭代,它使用next指针和。{ 成员指针,用于获取下一个next的地址。 因为迭代器的node<T>已经指向prev向后 迭代只需要将当前node<T>替换为 node<T>元素。

最后,这会留下一个保持开头和结尾的列表 的清单。处理双向访问和相应的 访问最后一个节点增加了一些复杂性和需要 实际上有一个专用节点。这是一个实现(其中 未经彻底测试:我可能搞砸了一些链接):

prev

只是为了一点点理智:这是一个简单打印列表的功能:

template <typename T, intrusive::node<T> T::*Link>
class intrusive::list
{
    node<T> content;

public:
    list() { this->content.prev = &this->content; }
    iterator<T, Link> begin() { return iterator<T, Link>(&this->content); }
    iterator<T, Link> end() { return iterator<T, Link>(this->content.prev); }

    T& front() { return *this->content.next; }
    T& back() { return *(this->content.prev->prev->next); }
    bool empty() const { return &this->content == this->content.prev; }
    void push_back(T& node) { this->insert(this->end(), node); }
    void push_front(T& node) { this->insert(this->begin(), node); }
    void insert(iterator<T, Link> pos, T& node) {
        (node.*Link).next = pos.current->next;
        ((node.*Link).next
         ? (pos.current->next->*Link).prev 
         : this->content.prev) = &(node.*Link);
        (node.*Link).prev = pos.current;
        pos.current->next = &node;
    }
    iterator<T, Link> erase(iterator<T, Link> it) {
        it.current->next = (it.current->next->*Link).next;
        (it.current->next
         ? (it.current->next->*Link).prev
         : this->content.prev) = it.current;
        return iterator<T, Link>(&(it.current->next->*Link));
    }
};

很少有其他方法可以避免做任何时髦的事情 访问封闭类。以上避免了几个条件。 假设我设法设置适当的链接更正代码 不会依赖任何实现定义或未定义的行为。

您可以使用以下列表:

template <typename T, intrusive::node<T> T::*Link>
std::ostream& intrusive::operator<< (std::ostream& out, intrusive::list<T, Link>& list)
{
    out << "[";
    if (!list.empty()) {
        std::copy(list.begin(), --list.end(), std::ostream_iterator<T>(out, ", "));
        out << list.back();
    }
    return out << "]";
}

答案 1 :(得分:4)

  

问题仍然存在:是否有可能使用双向迭代支持,没有未定义的行为以及没有“不必要的”内存开销来创建基于成员的侵入式列表?

您要做的是获取C ++对象的非静态数据成员,并将其转换为指向其包含类的指针。为此,您必须对表单进行一些操作:

node_ptr *ptr = ...;
auto p = reinterpret_cast<char*>(ptr) + offset;
T *t = reinterpret_cast<T*>(p);

要使此操作成为合法的C ++,您需要明确定义以下所有内容:

  1. 从节点的特定NSDM到包含它的T获取字节偏移量。
  2. 将该偏移量应用于指向成员的指针将导致指针值合法地转换为其拥有类型T
  3. 项目1只能通过offsetof在明确定义的C ++中实现;该标准提供没有其他方式来计算该偏移量。 offsetof要求类型(在本例中为T)为standard layout

    当然,offsetof需要成员的名称作为参数。并且您不能通过模板参数等传递参数名称;你必须通过宏来做到这一点。除非您愿意强迫用户以特定方式命名该成员。

    所以有你的限制:T必须是标准布局,你必须使用宏而不是直接函数调用,或者你必须强制用户使用成员的特定名称。如果你这样做,你应该是安全的,根据C ++。

    这是代码的样子:

    struct intrusive_list_node
    {
      intrusive_list_node *next;
      intrusive_list_node *prev;
    
      template<typename T, size_t offset> T *convert()
      {
        auto p = reinterpret_cast<char*>(this); //Legal conversion, preserves address.
        p -= offset; //Legal offset, so long as `offset` is correct
        return reinterpret_cast<T*>(p); //`p` has the same value representation as `T*` did originally, so should be legal.
      }
    }
    
    #define CONVERT_FROM_MEMBER(node, T, member_name) node->convert<T, offsetof(T, member_name)>()
    

答案 2 :(得分:2)

如果您不介意更改IntrusiveListNode类型,则可以让节点包含指向上一个/下一个节点的句柄 - 您只需要执行{{1}查找,而不是相反。

node -> handle

用法示例:

template<typename Node>
struct IntrusiveListHandle {
    Node *next = nullptr;
    // and Node* prev, etc ...
};

template<typename Node, IntrusiveListHandle<Node> Node::*handle>
struct IntrusiveList {
    Node *first;    

    static Node *next(Node *n) {
        auto h = (n->*handle).next;
    }
};

您应该不惜一切代价避免未定义的行为,因为编译器在利用UB进行优化时变得越来越聪明 - 现在正常工作的代码可能会突然中断下一次编译器更新。

我看到你提到了反向迭代。 #include <iostream> struct Test { IntrusiveListHandle<Test> handle; std::string value; Test(const std::string &v): value(v) {} }; template<typename IntrusiveList> void print(const IntrusiveList &list) { for (Test *n = list.first; n; n = list.next(n)) { std::cout << n->value << "\n"; } } int main() { Test hello("hello"); Test world("world!"); hello.handle.next = &world; IntrusiveList<Test, &Test::handle> list; list.first = &hello; print(list); } 不适用于此代码,但通常的方法是同时提供--end()begin()/end()对以允许反向迭代。

答案 3 :(得分:1)

我认为您可以使用CRTP实现这些好处:

#include <iostream>
using namespace std;

template<typename T>
struct ListNode
{
    ListNode<T>* next;

    // this would be nodeToItem in the list class
    T* value()
    {
        return static_cast<T*>(this);
    }
};

// This would be your abstract base class
struct A: public ListNode<A>
{
    A(int i): x(i) {}
    virtual ~A() = 0;
    int x;
};

inline A::~A() {}

struct B: public A
{
    B(int i): A(i) {}
    virtual ~B() {}
};

template<typename T>
class IntrusiveList {
public:
IntrusiveList(ListNode<T>* ptr): root(ptr) 
{
    ptr->next = nullptr;
}

void append(ListNode<T>* ptr)
{
    ptr->next = root;
    root = ptr;
}

ListNode<T>* begin() {return root;}
private:
ListNode<T>* root;
};

int main() {
    B b(10);
    B b2(11);
    IntrusiveList<A> l(&b);
    l.append(&b2);

    for(ListNode<A>* n=l.begin(); n != nullptr; n = n->next)
    {
         std::cout << n->value()->x << std::endl;
    }
    return 0;
}

通过在结构中使用ListNode指针数组,并将数组的索引作为模板参数或构造函数参数传递给列表类,可以使元素位于多个列表中。迭代器还需要将索引存储在ListNode数组中。

答案 4 :(得分:-1)

您可以使用其中一个成员的指针轻松获取原始对象,而无需调用UB。为什么你绝对不能?因为IntrusiveListNode可以在任何地方举行。没有关于特定IntrusiveListNode保存在T中的线索和另一个证明您不能这样做的证据:编译器无法知道发送到您的函数的节点是否真的保存在{ {1}}。你想要做什么未定义的行为。执行此操作的正确方法是在T中添加指向其容器的指针。

IntrusiveListNode

如果您无法使用template<typename T> struct IntrusiveListNode { IntrusiveListNode * next_; IntrusiveListNode * prev_; T* item_; }; template <typename T, IntrusiveListNode<T> T::*member> class IntrusiveList { // snip ... private: T & nodeToItem_(IntrusiveListNode<T> & node) { return *(node->item_); } IntrusiveListNode<T> root_; }; 模板,则可以使用IntrusiveListNode代替void*

您可以看到侵入式链接列表here

的实现示例

答案 5 :(得分:-1)

使用模板很难做到。可以使用宏,因此所需的成员_next,_prev等都在对象本身的范围内,而不是在单独的模板对象中。 使用宏可以避免每次都输入非常相似的代码。 事实上,几年前我创建了一个Case工具“ClassBuilder”(http://sourceforge.net/projects/classbuilder/),它使用宏来编写代码来创建使用侵入式链表的数据结构。 在我工作的区域,正常的模板链接列表只是缓慢的方式。在我们的业务中,使用非常大的核心数据结构是很正常的,这些结构也是非常动态的。因此在列表上进行了大量删除和添加以及搜索。 使用该工具完全从实际实现中抽象出来,您只需创建类图并从那里生成代码。 在我们所做的一个相对简单的测试案例中,对于使用“普通”STL类型实现的C ++解决方案,生成代码的运行时性能为40s和400s。运行几个小时后,同一测试用例的C#实现中止。它的实现类似于STL的实现,但垃圾收集器非常难以实现。由于测试用例的动态行为,所有可回收的内存只能在完整扫描中回收。