c – 构建向量以允许未初始化的存储

假设我想构建一个
unlike std::vector允许未初始化存储的向量容器.容器的使用,比如vec< T>,大致如下:

>用户明确声明向量应分配N个未初始化的元素,如:

vec< T> a(N,no_init);
>在数据已知的某个时刻,用户使用参数args …显式初始化位置n的元素:

a.init(n,args …);
> OR,等效地,手动构造元素:

新的(& a [n])T(args ……);
>其他操作可能会更大规模地初始化或复制(如std :: uninitialized_copy),但这只是为了方便;基本的底层操作是一样的.
>完成一些任务后,向量可能会留下一些初始化的元素,而其他元素则不会.向量不包含任何额外的信息,因此最终,在释放内存之前,它要么无论如何都要破坏所有元素,要么只根据T来破坏.

我很确定这可以做到,只是我不确定后果.当然,我们希望这种结构对所有类型都是安全的,假设用户在构造之前不尝试使用未初始化的元素.这可能听起来像一个强有力的假设,但仅在向量范围内访问元素并不是一个假设,它是如此常见.

所以我的问题是:

>对于哪种类型,允许这种未初始化的操作是安全的,如在vec< T>中那样.一(NO_INIT)?我猜is_pod会好的,也很可能也是非常好的.我不想提出比必要更多的限制.
>是否应始终执行销毁或仅针对某些类型执行销毁?同样的约束是否可以如上所述? is_trivially_destructible怎么样?这个想法是破坏一个尚未构建的元素,反之亦然(不破坏构造元素)应该不会造成伤害.
>除了向用户承担更多责任的明显风险之外,这次尝试是否存在重大缺陷?

重点是,当用户确实需要这样的性能功能时,较低级别的解决方案(如std :: get_temporary_buffer)或手动分配(例如使用operator new())在泄漏方面可能更具风险.我知道std :: vector :: emplace_back()但这真的不是一回事.

最佳答案 回答问题:

>对T没有限制:如果它适用于标准容器,它适用于您的.
> destroy是有条件的,如果std :: is_trivially_destructible< T>你可以静态地禁用它,否则你必须跟踪构造的元素并且只删除那些实际构造的元素.
>我没有看到你的想法存在重大缺陷,但要确保它是值得的:描述你的用例并检查你是否真的花了很多时间来初始化元素.

我假设你将容器实现为一个大小为()* sizeof(T)的连续内存块.此外,如果必须调用元素的析构函数,即!std :: is_trivially_destructible< T>,则必须启用其他存储,例如std :: vector< bool> size()元素用于标记要销毁的元素.

基本上,如果T是可以轻易破坏的,那么只需在用户提出要求并且不打扰任何东西时进行初始化.此外,事情有点棘手,您需要跟踪构建的元素和未初始化的元素,以便您只销毁所需的元素.

>增加尺寸或创建容器:

> if!std :: is_trivially_destructible< T>相应地调整标志存储
>内存分配
>可选的初始化取决于用户询问的内容:

> no_init => if!std :: is_trivially_destructible< T>,将元素标记为未初始化.否则什么都不做.
>(Args …)=>如果std :: is_constructible< T,class … Args>为每个元素调用该构造函数.如果!std :: is_trivially_destructible< T>,则将元素标记为已构造.

>缩小尺寸或破坏容器:

>可选的销毁:

>如果std :: is_trivially_destructible< T>没做什么
>对于每个元素,如果它被标记为构造,则调用其析构函数

>内存释放
>如果!std :: is_trivially_destructible< T>相应地调整标志存储

从性能的角度来看,如果T是可以轻易破坏的,那么事情就会很好.如果它有一个析构函数,那么事情会更加严重:你获得了一些构造函数/析构函数调用,但是你需要维护额外的标志存储 – 最后它取决于你的构造函数/析构函数是否足够复杂.

也像评论中建议的那样,您可以使用基于std :: unordered_map的关联数组,添加size_t vector_size字段,实现调整大小和覆盖大小.这样,甚至不会存储未初始化的元素.另一方面,索引会更慢.

点赞