草庐IT

c++ - C++ 中灵活数组成员的可移植仿真?

coder 2024-02-03 原文

我正在写一个 skip list .

我有什么:

template<typename T>
struct SkipListNode
{
    T data;
    SkipListNode* next[32];
};

这段代码的问题在于它浪费了空间——它要求所有节点都包含 32 个指针。特别是考虑到在典型的列表中,一半的节点只需要一个指针。

C 语言有一个称为灵活数组成员的巧妙特性可以解决这个问题。如果它存在于 C++ 中(即使对于普通类),我可以编写如下代码:

template<typename T>
struct SkipListNode
{
    alignas(T) char buffer[sizeof(T)];
    SkipListNode* next[];
};

然后用工厂函数手动创建节点,并在删除元素时销毁它们。

这带来了问题 - 我如何可移植模拟这样的功能,而没有 C++ 中的未定义行为?

我考虑过 malloc 缓冲区,然后手动适本地操作偏移量 - 但它太容易违反对齐要求 - 如果你 malloc(sizeof(char) + sizeof(void *)*5),指针未对齐。另外,我什至不确定这种手工创建的缓冲区是否可以移植到 C++。

请注意,我不需要确切的语法,甚至不需要易用性 - 这是一个节点类,在跳过列表类内部,它根本不是界面的一部分。

最佳答案

这是我写的实现,based on R. Martinho Fernandes's idea - 它构造了一个恰好在特定位置具有正确大小和对齐方式的缓冲区(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);
}

输出:

Hello world
A
B
C

更长的解释:

这段代码的重点是动态创建一个节点,而不是直接使用类型(类型删除)。此节点存储一个对象,N指针,带有 N运行时变量。

您可以像使用特定类型的内存一样使用任何内存,前提是:

  1. 尺寸正确
  2. 对齐正确
  3. (仅限不可平凡构造的类型)您在使用前手动调用构造函数
  4. (仅限不可破坏的类型)您在使用后手动调用析构函数

事实上,每次调用malloc时,您都依赖于此:

// 1. Allocating a block
int* p = (int*)malloc(5 * sizeof *p);
p[2] = 42;
free(p);

在这里,我们处理由 malloc 返回的内存块就好像它是一个整数数组。由于这些保证,这必须有效:

  • malloc返回保证对任何对象类型正确对齐的指针。
  • 如果你的指针p指向对齐内存,(int*)((char*)p + sizeof(int)) (或 p + 1 ,这是等效的)也是如此。

动态创建的节点必须有足够的大小来包含N ErasedNodePointer s(这里用作句柄)和一个大小为 T 的对象.这可以通过在 create_node 中分配足够的内存来满足。函数 - 它将分配 sizeof(T) + sizeof(ErasedNodePointer<T>)*N字节或更多,但不少于

这是第一步。第二个是现在我们提取相对于 block 开头的所需位置。那就是AlignmentExtractor<T>进来了。

AlignmentExtractor<T>是我用来确保正确对齐的虚拟结构:

// 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;

我如何得到指针的位置并不重要,只要我遵守指针运算规则即可。

这里的假设是:

  • 我可以将任何指针转换为void*并返回。
  • 我可以将任何指针转换为char*并返回。
  • 我可以对结构进行操作,就好像它是一个大小等于结构大小的字符数组。
  • 我可以使用指针运算来指向数组的任何元素。

这些都是C++标准保证的。

现在,在我分配了足够大小的 block 之后,我用 offsetof(AlignmentExtractor<T>, next) 计算偏移量并将其添加到指向 block 的指针。我们“假装”(就像代码“1.分配 block ”假装它有一个整数数组一样)结果指针指向数组的开头。该指针已正确对齐,否则代码“2. 查找位置”无法访问 next。由于访问未对齐而导致的数组。

如果您有一个标准布局类型的结构,则指向该结构的指针与该结构的第一个成员具有相同的地址。 AlignmentExtractor<T>是标准布局。

但这还不是全部 - 要求 1. 和 2. 已得到满足,但我们需要满足要求 3. 和 4. - 节点中的数据不必是微不足道的可构造或可破坏的。这就是为什么我们使用 placement-new 来构造数据 - create_node使用可变参数模板和完美转发将参数转发给构造函数。数据在 destroy_node 中被销毁通过调用析构函数来实现功能。

关于c++ - C++ 中灵活数组成员的可移植仿真?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/29682605/

有关c++ - C++ 中灵活数组成员的可移植仿真?的更多相关文章

  1. ruby-on-rails - 如何优雅地重启 thin + nginx? - 2

    我的瘦服务器配置了nginx,我的ROR应用程序正在它们上运行。在我发布代码更新时运行thinrestart会给我的应用程序带来一些停机时间。我试图弄清楚如何优雅地重启正在运行的Thin实例,但找不到好的解决方案。有没有人能做到这一点? 最佳答案 #Restartjustthethinserverdescribedbythatconfigsudothin-C/etc/thin/mysite.ymlrestartNginx将继续运行并代理请求。如果您将Nginx设置为使用多个上游服务器,例如server{listen80;server

  2. ruby-on-rails - Rails 模型——非持久类成员或属性? - 2

    对于Rails模型,是否可以/建议让一个类的成员不持久保存到数据库中?我想将用户最后选择的类型存储在session变量中。由于我无法从我的模型中设置session变量,我想将值存储在一个“虚拟”类成员中,该成员只是将值传递回Controller。你能有这样的类(class)成员吗? 最佳答案 将非持久属性添加到Rails模型就像任何其他Ruby类一样:classUser扩展解释:在Ruby中,所有实例变量都是私有(private)的,不需要在赋值前定义。attr_accessor创建一个setter和getter方法:classUs

  3. ruby - 使用 `+=` 和 `send` 方法 - 2

    如何将send与+=一起使用?a=20;a.send"+=",10undefinedmethod`+='for20:Fixnuma=20;a+=10=>30 最佳答案 恐怕你不能。+=不是方法,而是语法糖。参见http://www.ruby-doc.org/docs/ProgrammingRuby/html/tut_expressions.html它说Incommonwithmanyotherlanguages,Rubyhasasyntacticshortcut:a=a+2maybewrittenasa+=2.你能做的最好的事情是:

  4. ruby - 如何计算 Liquid 中的变量 +1 - 2

    我对如何计算通过{%assignvar=0%}赋值的变量加一完全感到困惑。这应该是最简单的任务。到目前为止,这是我尝试过的:{%assignamount=0%}{%forvariantinproduct.variants%}{%assignamount=amount+1%}{%endfor%}Amount:{{amount}}结果总是0。也许我忽略了一些明显的东西。也许有更好的方法。我想要存档的只是获取运行的迭代次数。 最佳答案 因为{{incrementamount}}将输出您的变量值并且不会影响{%assign%}定义的变量,我

  5. arrays - Ruby 数组 += vs 推送 - 2

    我有一个数组数组,想将元素附加到子数组。+=做我想做的,但我想了解为什么push不做。我期望的行为(并与+=一起工作):b=Array.new(3,[])b[0]+=["apple"]b[1]+=["orange"]b[2]+=["frog"]b=>[["苹果"],["橙子"],["Frog"]]通过推送,我将推送的元素附加到每个子数组(为什么?):a=Array.new(3,[])a[0].push("apple")a[1].push("orange")a[2].push("frog")a=>[[“苹果”、“橙子”、“Frog”]、[“苹果”、“橙子”、“Frog”]、[“苹果”、“

  6. ruby - 在 Ruby 中,为什么 Array.new(size, object) 创建一个由对同一对象的多个引用组成的数组? - 2

    如thisanswer中所述,Array.new(size,object)创建一个数组,其中size引用相同的object。hash=Hash.newa=Array.new(2,hash)a[0]['cat']='feline'a#=>[{"cat"=>"feline"},{"cat"=>"feline"}]a[1]['cat']='Felix'a#=>[{"cat"=>"Felix"},{"cat"=>"Felix"}]为什么Ruby会这样做,而不是对object进行dup或clone? 最佳答案 因为那是thedocumenta

  7. += 的 Ruby 方法 - 2

    有没有办法让Ruby能够做这样的事情?classPlane@moved=0@x=0defx+=(v)#thisiserror@x+=v@moved+=1enddefto_s"moved#{@moved}times,currentxis#{@x}"endendplane=Plane.newplane.x+=5plane.x+=10putsplane.to_s#moved2times,currentxis15 最佳答案 您不能在Ruby中覆盖复合赋值运算符。任务在内部处理。您应该覆盖+,而不是+=。plane.a+=b与plane.a=

  8. ruby - Sinatra + Heroku + Datamapper 使用 dm-sqlite-adapter 部署问题 - 2

    出于某种原因,heroku尝试要求dm-sqlite-adapter,即使它应该在这里使用Postgres。请注意,这发生在我打开任何URL时-而不是在gitpush本身期间。我构建了一个默认的Facebook应用程序。gem文件:source:gemcuttergem"foreman"gem"sinatra"gem"mogli"gem"json"gem"httparty"gem"thin"gem"data_mapper"gem"heroku"group:productiondogem"pg"gem"dm-postgres-adapter"endgroup:development,:t

  9. ruby - Ruby 中字符串运算符 + 和 << 的区别 - 2

    我是Ruby和这个网站的新手。下面两个函数是不同的,一个在函数外修改变量,一个不修改。defm1(x)x我想确保我理解正确-当调用m1时,对str的引用被复制并传递给将其视为x的函数。运算符当调用m2时,对str的引用被复制并传递给将其视为x的函数。运算符+创建一个新字符串,赋值x=x+"4"只是将x重定向到新字符串,而原始str变量保持不变。对吧?谢谢 最佳答案 String#+::str+other_str→new_strConcatenation—ReturnsanewStringcontainingother_strconc

  10. ruby-on-rails - 可移植 Ruby on Rails 环境 - 2

    我给自己买了一个新的8gigUSBkey,我正在寻找一个合适的解决方案来拥有一个可移植RoR环境来学习。我在谷歌上搜索了一下,发现了一些可能性,但我很想听听一些现实生活中的经历和意见。谢谢! 最佳答案 我喜欢InstantRails,非常容易使用,无需安装程序,也不会修改您的系统环境。 关于ruby-on-rails-可移植RubyonRails环境,我们在StackOverflow上找到一个类似的问题: https://stackoverflow.com/q

随机推荐