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;