C++ C+中灵活阵列成员的可移植仿真+;?
我正在写一封信 我所拥有的:C++ C+中灵活阵列成员的可移植仿真+;?,c++,c++11,C++,C++11,我正在写一封信 我所拥有的: template<typename T> struct SkipListNode { T data; SkipListNode* next[32]; }; 模板 结构SkipListNode { T数据; SkipListNode*next[32]; }; 这段代码的问题在于它浪费了空间——它要求所有节点都包含32个指针。特别是考虑到在典型的列表中,一半的节点只需要一个指针 C语言有一个称为flexiblearray成员的简洁特性,可
template<typename T>
struct SkipListNode
{
T data;
SkipListNode* next[32];
};
模板
结构SkipListNode
{
T数据;
SkipListNode*next[32];
};
这段代码的问题在于它浪费了空间——它要求所有节点都包含32个指针。特别是考虑到在典型的列表中,一半的节点只需要一个指针
C语言有一个称为flexiblearray成员的简洁特性,可以解决这个问题。如果它存在于C++中(甚至对于平凡的类),我可以编写这样的代码:
template<typename T>
struct SkipListNode
{
alignas(T) char buffer[sizeof(T)];
SkipListNode* next[];
};
模板
结构SkipListNode
{
alignas(T)字符缓冲区[sizeof(T)];
SkipListNode*下一个[];
};
然后使用工厂函数手动创建节点,并在删除元素时销毁它们
这带来了问题——我如何在不使用C++中的未定义行为的情况下,灵活地模仿这样的功能? 我考虑过
malloc
手动调整缓冲区,然后适当地操作偏移量-但是很容易违反对齐要求-如果malloc(sizeof(char)+sizeof(void*)*5)
,指针是未对齐的。而且,我甚至不确定这种手工创建的缓冲区是否可以移植到C++。
请注意,我不需要精确的语法,甚至不需要易于使用-这是一个节点类,在skip list类内部,它根本不属于接口的一部分 这是我编写的实现,-它构造了一个缓冲区,恰好在特定位置具有正确的大小和对齐方式(使用
AlignmentExtractor
提取指针数组的偏移量,以确保缓冲区中的指针具有正确对齐方式)。然后,使用placement new在缓冲区中构造类型
T
不直接用于AlignmentExtractor
,因为offsetof
需要标准布局类型
#include <cstdlib>
#include <cstddef>
#include <utility>
template<typename T>
struct ErasedNodePointer
{
void* ptr;
};
void* allocate(std::size_t size)
{
return ::operator new(size);
}
void deallocate(void* ptr)
{
return ::operator delete(ptr);
}
template<typename T>
struct AlignmentExtractor
{
static_assert(alignof(T) <= alignof(std::max_align_t), "extended alignment types not supported");
alignas(T) char data[sizeof(T)];
ErasedNodePointer<T> next[1];
};
template<typename T>
T& get_data(ErasedNodePointer<T> node)
{
return *reinterpret_cast<T*>(node.ptr);
}
template<typename T>
void destroy_node(ErasedNodePointer<T> node)
{
get_data(node).~T();
deallocate(node.ptr);
}
template<typename T>
ErasedNodePointer<T>& get_pointer(ErasedNodePointer<T> node, int pos)
{
auto next = reinterpret_cast<ErasedNodePointer<T>*>(reinterpret_cast<char*>(node.ptr) + offsetof(AlignmentExtractor<T>, next));
next += pos;
return *next;
}
template<typename T, typename... Args>
ErasedNodePointer<T> create_node(std::size_t height, Args&& ...args)
{
ErasedNodePointer<T> p = { nullptr };
try
{
p.ptr = allocate(sizeof(AlignmentExtractor<T>) + sizeof(ErasedNodePointer<T>)*(height-1));
::new (p.ptr) T(std::forward<T>(args)...);
for(std::size_t i = 0; i < height; ++i)
get_pointer(p, i).ptr = nullptr;
return p;
}
catch(...)
{
deallocate(p.ptr);
throw;
}
}
#include <iostream>
#include <string>
int main()
{
auto p = create_node<std::string>(5, "Hello world");
auto q = create_node<std::string>(2, "A");
auto r = create_node<std::string>(2, "B");
auto s = create_node<std::string>(1, "C");
get_pointer(p, 0) = q;
get_pointer(p, 1) = r;
get_pointer(r, 0) = s;
std::cout << get_data(p) << "\n";
std::cout << get_data(get_pointer(p, 0)) << "\n";
std::cout << get_data(get_pointer(p, 1)) << "\n";
std::cout << get_data(get_pointer(get_pointer(p, 1), 0)) << "\n";
destroy_node(s);
destroy_node(r);
destroy_node(q);
destroy_node(p);
}
详细解释:
这段代码的要点是动态创建一个节点,而不直接使用类型(类型擦除)。此节点在运行时使用N
变量存储对象和N
指针
您可以像使用特定类型的内存一样使用任何内存,前提是:
malloc
,您都依赖于此:
// 1. Allocating a block
int* p = (int*)malloc(5 * sizeof *p);
p[2] = 42;
free(p);
这里,我们将malloc
返回的内存块视为int数组。由于这些保证,这必须起作用:
返回保证对任何对象类型正确对齐的指针malloc
- 如果指针
指向对齐的内存,p
(或(int*)((char*)p+sizeof(int))
,这是等效的)也会指向对齐的内存p+1
N
ErasedNodePointer
s(此处用作句柄)和一个大小为T
的对象。这可以通过在create_node
函数中分配足够的内存来实现-它将分配sizeof(T)+sizeof(ErasedNodePointer)*N
字节或更多,但不少于
这是第一步。第二个是现在我们提取相对于块开始的所需位置。这就是AlignmentExtractor
的作用
AlignmentExtractor
是一个虚拟结构,用于确保正确对齐:
// 2. Finding position
AlignmentExtractor<T>* p = (AlignmentExtractor<T>*)malloc(sizeof *p);
p->next[0].ptr = nullptr;
// or
void* q = (char*)p + offsetof(AlignmentExtractor<T>, next);
(ErasedTypePointer<T>*)q->ptr = nullptr;
//2。定位
AlignmentExtractor*p=(AlignmentExtractor*)malloc(sizeof*p);
p->next[0]。ptr=nullptr;
//或
void*q=(char*)p+偏移量(AlignmentExtractor,next);
(ErasedTypePointer*)q->ptr=nullptr;
不管我如何得到指针的位置,只要我遵守指针运算的规则
这里的假设是:
- 我可以将任何指针投射到
并返回void*
- 我可以将任何指针投射到
并返回char*
- 我可以对结构进行操作,就像它是大小等于结构大小的字符数组一样
- 我可以使用指针算法指向数组的任何元素
这些都是用C++标准保证的。 现在,在分配了足够大的块之后,我使用
offsetof(AlignmentExtractor,next)
计算偏移量,并将其添加到指向块的指针中。我们“假装”(就像代码“1.分配块”假装它有一个int数组一样)结果指针指向数组的开头。此指针已正确对齐,因为否则代码“2.查找位置”无法访问next
数组,因为访问未对齐
如果具有标准布局类型的结构,则指向该结构的指针的地址与该结构的第一个成员的地址相同<代码>对齐提取器是标准布局
但这并不是全部——要求1。二,。我们很满意,但我们需要满足要求3。和4节点中的数据不必是可构造或可破坏的。这就是为什么我们使用placement new来构造数据,
create_节点
使用可变模板和完美转发将参数转发给构造函数。通过调用析构函数,在destroy\u节点中销毁数据。您尝试过向量吗std::vector next
如果固定大小的跳过列表覆盖了存储的最大数据量,那么它就很好了。它没有为可变大小的节点引入额外的间接寻址。然而,如果数据量很小,那就是对存储的浪费。您可以使用一个模板。@MattG的问题与第一种方法相同。@milleniumbug可能template struct SkipListNode
malloc
保证为任何对象返回一个适当对齐的指针(tha
// 2. Finding position
AlignmentExtractor<T>* p = (AlignmentExtractor<T>*)malloc(sizeof *p);
p->next[0].ptr = nullptr;
// or
void* q = (char*)p + offsetof(AlignmentExtractor<T>, next);
(ErasedTypePointer<T>*)q->ptr = nullptr;