【问题标题】:building a vector to allow uninitialized storage构建一个向量以允许未初始化的存储
【发布时间】:2014-02-05 18:37:58
【问题描述】:

假设我想构建一个矢量容器,unlike std::vector,允许未初始化的存储。容器的用法,比如vec <T>,大概是这样的:

  • 用户明确指出向量应该像这样分配 N 个未初始化的元素:

    vec <T> a(N, no_init);

  • 在数据已知的某个时刻,用户使用参数args... 显式初始化位置n 的元素:

    a.init(n, args...);

  • 或者,等效地,手动构造元素:

    new (&a[n]) T(args...);

  • 其他操作可能会更大规模地初始化或复制(如std::uninitialized_copy),但这只是为了方便;基本的底层操作是一样的。

  • 完成某些任务后,向量可能会留下一些已初始化的元素,而另一些则不会。该向量不包含任何额外信息,因此最终,在释放内存之前,它要么销毁所有元素,要么仅根据T 销毁。

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

所以我的问题是:

  1. vec <T> a(no_init) 中允许这种未初始化操作对于哪些类型是安全的?我猜is_pod 可以,很可能is_trivial 也可以。我不想设置不必要的限制。

  2. 应该始终执行销毁还是只针对某些类型执行销毁?与上面相同的约束可以吗? is_trivially_destructible 怎么样?这个想法是,破坏一个构造的元素,反之亦然(破坏一个构造的元素)应该没有害处。

  3. 除了给用户带来更多责任的明显风险之外,这种尝试是否存在重大缺陷?

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

【问题讨论】:

  • 在调用reserve 之后,您想开发一个类似于std::vector 的容器吗?来自this ref:“请求向量容量至少足以包含 n 个元素。” .
  • 听起来它根本不是你想要的向量。关联容器有什么问题?
  • @wesley.mesquita 仅部分。是的,我想要reserve 所做的分配,但我也希望数据完全可访问。例如。 size() 应该包括这些分配的元素。
  • 破坏对我来说不是很清楚......这只有在容器知道哪些元素是真实的以及哪些是未初始化的情况下才有效。否则,正如你所说,你需要is_trivially_destructible
  • 如果避免vector::resize() 和/或vector::vector(size_t) 执行的值初始化足以解决您的问题,请查看this answer

标签: c++ vector initialization typetraits destruction


【解决方案1】:

回答问题:

  1. T 没有限制:如果它适用于标准容器,它适用于您的容器。
  2. 销毁是有条件的,如果std::is_trivially_destructible<T>,您可以静态禁用它,否则您必须跟踪构造的元素并仅删除实际构造的元素。
  3. 我认为您的想法没有重大缺陷,但请确保它是值得的:分析您的用例并检查您是否确实花费了大量时间来初始化元素。

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

基本上,如果T 可以简单地破坏,您只需在用户询问时进行初始化,而无需费心破坏任何东西。否则,事情会有些棘手,您需要跟踪构建了哪些元素以及未初始化的元素,以便只销毁需要的元素。

  • 扩大规模或创建容器
    1. 如果!std::is_trivially_destructible<T>相应地调整标志存储
    2. 内存分配
    3. 根据用户的要求进行可选初始化:
      • no_init => 如果!std::is_trivially_destructible<T>,将元素标记为未初始化。否则什么都不做。
      • (Args...) => 如果std::is_constructible<T, class... Args> 为每个元素调用该构造函数。如果是!std::is_trivially_destructible<T>,则将元素标记为已构建。
  • 缩小尺寸或销毁容器
    1. 可选销毁:
      • 如果std::is_trivially_destructible<T>什么都不做
      • 对于每个元素,如果它被标记为已构造,则调用其析构函数
    2. 内存释放
    3. 如果 !std::is_trivially_destructible<T> 相应地调整存储标志的大小

从性能的角度来看,如果T 可以简单地破坏,那就太好了。如果它有一个析构函数,事情就更加复杂了:你获得了一些构造函数/析构函数调用,但是你需要维护额外的标志存储——最终这取决于你的构造函数/析构函数是否足够复杂。

也像 cmets 中的一些建议,您可以只使用基于 std::unordered_map 的关联数组,添加 size_t vector_size 字段,实现 resize 并覆盖 size。这样,甚至不会存储未初始化的元素。另一方面,索引会更慢。

【讨论】:

  • 非常感谢您的回答!我觉得你走得太远了。为了简单起见,我在我的问题中提到“向量不包含任何额外信息”,因此没有标志,并且决策是“平坦的”而不是“每个元素”。这意味着对于某些类型Tno_init 应该被禁用,因此行为应该就像std::vector。问题大致是“那些类型是什么?”和“在没有标志的情况下销毁时我应该怎么做?是否销毁元素?”。鉴于这些限制,您能否详细说明一下?
  • 还有一件事。一个类型可能是is_trivially_destructible,但它可能有自己的构造函数,更糟糕的是,构造函数可能会分配一些资源。这可能与rule of three 背道而驰,但仍然可能意味着我应该更加保守并额外使用is_trivially_constructible 甚至is_trivial。这是正确的吗?我不得不承认,当我试图了解所有这些特征的确切定义以及对我的问题的影响时,我总是迷失了方向。
  • 如果用户定义了一个自定义析构函数来释放一些内存,那么这个类就不能被简单地破坏:cplusplus.com/reference/type_traits/is_trivially_destructible 如果没有这样的析构函数,但构造函数仍然分配了一些内存,那么它就是内存泄漏以及用户端的错误,而不是容器的逻辑。
  • 更保守地只处理可构造的类型并不能解决问题,因为该类型可能仍然是一个具有某些泄漏内存方法的类。甚至 is_trivial 也允许可能做坏事的方法。实际上,这里唯一万无一失的选择是限制为 is_integral 之类的东西。但是从设计的角度来看,假设包含的类型被破坏并不是容器的责任......
  • 好的,看看我是否做对了。我只检查类型是否为is_trivially_destructible,在这种情况下我允许no_init 操作并且从不破坏任何元素,没有标志,也不管元素是否被初始化。如果同一类型确实在构造函数或任何其他方法中分配资源,那么它只是一个损坏的类型,无论如何都会泄漏,而不是容器的责任。另一方面,如果类型不是可轻易破坏的,则表现得像std::vector。对吗?
猜你喜欢
  • 2010-09-10
  • 1970-01-01
  • 2014-07-06
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-12-25
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多