几年来我第三次发现自己需要一个不允许提升的项目的侵入性链表(问管理......)。
我第三次发现侵入式链表实现我的工作完美,但我真的不喜欢它使用未定义的行为 - 即将指针转换为列表节点到指向包含该列表节点的对象。
这个糟糕的代码目前看起来像这样:
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年 - 有没有办法在不调用未定义的行为的情况下做到这一点?
编辑: 我想在这里总结一些建议的解决方案(涉及列表的不同结构):
生活在未定义的行为中,因为该语言似乎具有任意限制,可以防止反向使用成员指针。
在IntrusiveListNode
中存储指向包含类的附加指针。这当前可能是最干净的解决方案(不需要更改接口),但在每个列表节点中都需要第三个指针(可能有小的优化)。
从IntrusiveListNode
导出并使用static_cast
。在boost中,这是侵入式链表的base_hook
版本。我想坚持使用member_hook
版本以避免引入多重继承。
存储指向下一个和上一个包含类的指针,而不是指向IntrusiveListNode
中下一个和上一个列表节点的指针。这使得在侵入列表中创建根节点变得困难。列表必须包含T
的完整实例化(这是不可能的,例如,如果T
是抽象的),或者列表的末尾需要是空指针(这会破坏{{1}只允许前向迭代)。
Boost侵入式列表有--list.end()
版本可以某种方式工作,但实现尚未被理解(并且它可能还依赖于未定义的行为)。
问题仍然存在:是否有可能使用双向迭代支持,没有未定义的行为以及没有“不必要的”内存开销来创建基于成员的侵入式列表?
答案 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 ++,您需要明确定义以下所有内容:
T
获取字节偏移量。T
。项目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的实现,但垃圾收集器非常难以实现。由于测试用例的动态行为,所有可回收的内存只能在完整扫描中回收。