【问题标题】:Is it a good idea to base a non-owning bit container on std::vector<bool>? std::span?在 std::vector<bool> 上建立一个非拥有位容器是个好主意吗?标准::跨度?
【发布时间】:2018-11-23 14:28:53
【问题描述】:

在我的几个项目中,我越来越需要处理内存中的连续位序列 - 高效 (*)。到目前为止,我已经编写了一堆可内联的独立函数,以选择“位容器”类型(例如uint32_t)为模板,用于获取和设置位,将“或”和“和”应用于它们的值,定位容器,将位长度转换为字节大小或容器长度等......看起来是编写类的时间。

我知道 C++ 标准库有一个专门化的 std::vector&lt;bool&gt;,许多人认为这是一个设计缺陷——因为它的迭代器没有暴露实际的 bools,而是代理对象。无论这对于专业化来说是个好主意还是坏主意,这绝对是我正在考虑的事情 - 一个明确的位代理类,希望它“总是”被优化掉(用constexpr,@987654326 进行很好的润滑@ 和 inline)。因此,我正在考虑可能从标准库实现之一改编std::vector 代码。

另一方面,我的预期课程:

  • 永远不会拥有数据/位 - 它将接收起始位容器地址(假设对齐)和位长度,并且不会分配或释放。
  • 它将无法动态或以其他方式调整数据大小 - 即使在保留与 std::vector::resize() 相同的空间量时也不行;它的长度将在其生命周期/范围内固定。
  • 它应该对堆一无所知(并且在没有堆时工作)

从这个意义上说,它更像是位的跨度类。那么也许从跨度开始呢?我不知道,跨度仍然不标准;并且跨度中没有代理...

那么,什么是我实现的良好基础(edit: 不是基类)? std::vector&lt;bool&gt;? std::span?两个都?没有?或者 - 也许我正在重新发明轮子,而这已经是一个已解决的问题?

注意事项:

  • 位序列长度在运行时已知,而不是编译时;否则,正如@SomeProgrammerDude 建议的那样,我可以使用std::bitset
  • 我的班级不需要“be-a”跨度或“be-a”向量,所以我没有考虑专门研究其中的任何一个。

(*) - 到目前为止,SIMD 效率不高,但可能会在以后出现。此外,这可以用于我们不 SIMDize 而是假装通道是正确线程的 CUDA 代码中。

【问题讨论】:

  • std::bitset?而且我真的不建议专门化std::vector,从那时起你基本上只是要重新实现std::vector&lt;bool&gt;。相反,您可能希望创建自己的课程,以更好地满足您的要求,并且可以足够开放以纳入您的未来计划。
  • @Someprogrammerdude:请参阅我的编辑中的“注释”。
  • 根据我的想象,我将从std::span 实现开始——可以在网上找到——并从vector&lt;bool&gt; 借用位代理。
  • 为什么不在你的类中存储原始 ptr 和长度,然后提供一些函数来检查位?因为它是非欠的,所以不需要副本。
  • @BeeOnRope:因为我想使用代理,所以代码的结构会有点不同。不过谢谢。

标签: c++ stdvector class-design c++20 std-span


【解决方案1】:

而不是std::vectorstd::span,我怀疑你的类的实现将与std::bitset 有更多的共同点,因为它几乎是一样的,除了(固定的)运行时确定的大小。

事实上,您可能会采用典型的std::bitset 实现并将&lt;size_t N&gt; 模板参数作为size_t size_ 成员(或您喜欢的任何名称)移动到类中,您将拥有动态位集类几乎没有变化。您可能想摆脱任何您认为不合适的东西,例如采用 std::string 和朋友的构造函数。

最后一步是删除底层数据的所有权:基本上,您将在构造函数中删除底层数组的创建,并使用一些指针维护现有数组的视图。

如果您的客户不同意用于存储的底层无符号整数类型(您称之为“位容器”),那么您可能还需要使您的类成为这种类型的模板,尽管如果这样做会更简单每个人都同意说uint64_t

std::vector&lt;bool&gt; 而言,您不需要太多:vector 所做的一切都是您想要的,std::bitset 可能也会做:vector 添加的主要内容是动态增长 - 但是你说过你不想要那个。 vector&lt;bool&gt; 具有代表单个位的代理对象概念,但 std::bitset 也是如此。

来自std::span,您认为基础数据不具有所有权,但我认为这实际上并不代表很多基础代码。如果有用,您可能需要考虑使用std::span 方法,让或者 编译时已知大小 运行时提供的大小(由Extent == std::dynamic_extent 表示)适合您(主要是如果您有时使用编译时大小并且可以专门化一些方法以在这种情况下更有效)。

【讨论】:

  • 实际上,对于 uint64_t 来说,每个人都同意并不会更简单,原因有两个: 1. 在某些非 x86 平台(例如 nVIDIA GPU)上使用它的成本很高 2. 有实际的出于某些实际考虑,需要针对不同的容器大小实例化此类。
  • @einpoklum 在这种情况下,您可以将其作为C(位容器类型)之上的模板类,以便客户端代码可以选择存储类型。它使实现稍微复杂化。一个缺点是具有不同基础C 选择的位集是不同的类型,因此即使公开相同的API,您也不能直接将dyn_bitset&lt;uint8_t&gt; 传递给期望dyn_bitset&lt;uint32_t&gt; 的东西。如果您将接受函数更改为使用模板参数,它将通过鸭子类型工作,尽管这也有一些缺点。
  • 是的,确实,我已经在问题中提到我正在对容器类型进行模板化。顺便说一句,当我有时间研究 std::bitset 代码并使用它时,我会决定是否接受你的答案(我已经 +1 了)。
  • @einpoklum - 很清楚,您说您现有的独立函数解决方案是在容器类型上模板化的。在有关问题的 cmets 中,我试图提取更多关于现有解决方案的信息,以及为什么你不想使用它,或者你想要什么,但可以这么说,你没有上钩。所以正如我所提到的,我写这篇文章或多或少好像你在做一个新的实现,所以像它是否是一个模板这样的事情需要重新评估。
  • 对于像bitset这样的通用类(包括你想要的半动态[1]位集),使用存储类型的模板会很烦人,因为每种存储类型都会导致不相关的对象,因此您希望就兼容性的常用类型达成一致,但您的用例可能会有所不同,更狭窄并且更倾向于包括类型中的存储(例如,为了性能)。 [1] 在这里,我使用“半动态”来表示运行时长度,但它在每个对象的构造时都是固定的,如您所愿。
猜你喜欢
  • 2018-08-10
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2010-10-10
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多