在固定的,无序的,拥有的数组中安全,惯用的销毁和压缩

Safe, idiomatic destroy-and-compact within fixed, unordered, owning array

本文关键字:压缩 安全 数组 无序 拥有      更新时间:2023-10-16

考虑一个数据结构,该数据结构包含固定尺寸的缓冲区,该缓冲区拥有某些任意非平凡类型的现场成员。缓冲区是无序的,但其内容是使用固定数组和计数器值连续存储的。破坏该数组中元素并用数组的最后一个元素替换其插槽以保持连续性的现代,惯用的C 方法是什么?我特别关心确保元素被适当破坏,并且替换被正确有效地移动。不幸的是,std::vector不是这里的选项。

这是我能想到的:

void destroy_replace(
    std::array<arbitrary, 20>& arr, 
    size_t& count,
    size_t index)
{
    SOME_ASSERT_MACRO(index < count);
    std::destroy_at(std::addressof(arr.at(index)));
    arr[index] = std::move(arr.at(count - 1));
    --count;
}

这是正确的吗?特别是std::destroy_at

需要举动吗?我应该/应该作为安置新移动构造函数吗?

操作顺序相对于例外保证了吗?

您应该使用位置新的移动构造函数,作为破坏对象的分配操作员,不确定。如果您没有这样的构造函数,则可以诉诸默认的构造函数,然后可以构建移动分配,但是应构造std::destroy_at之后的对象。

需要移动才能投入到RVALUE参考,因为.at是lvalue参考。

在这种情况下,提供异常保证的典型方法是需要移动构造函数或移动分配为noexcept。否则它不适合移动,您需要复制。

只需从最后一个元素移动分配即可。这并不是说您的代码实际上正在销毁网络上的任何内容(固定后 - 您必须在破坏元素后使用新的位置,因此您要销毁一个并创建另一个),而移动分配通常至少具有高效 - 如果没有多于销毁和重建。

有两种方法。您要么移动要删除要删除的元素,然后销毁最后一个,或然后移动删除最后一个元素。

template<class T, std::size_t N>
struct pseudo_array {
  using raw = std::aligned_storage_t<sizeof(T), alignof(T)>;
  std::array<raw, N> data;
  std::size_t highwater = 0;
  void erase( std::size_t i ) {
    std::launder( (T*)(data.data()+i) )->~T();
    --highwater;
    if (i != highwater) {
       auto* ptr_last = std::launder(  (T*)(data.data()+highwater) );
       ::new( (void*)(data.data()+i) ) T( std::move(*ptr_last ) );
       ptr_last->~T();
    }
  }
  // add const version
  T& operator[](std::size_t i) {
    return *std::launder( (T*)(data.data()+i) );
  }
  std::size_t size() const { return highwater; }
  template<class...Args>
  T& emplace( Args&&...args ) {
    void* where = (void*)(data.data()+highwater);
    T* ptr_elem = ::new(where) T(std::forward<Args>(args)...);
    ++highwater;
    return *ptr_elem;
  }
  // Care taken to keep invariants true while we destroy
  ~pseudo_array() {
    while(highwater > 0) {
      --highwater;
      std::launder( (T*)(data.data()+highwater) )->~T();
    }
  }
};

替代擦除:

 void erase( std::size_t i ) {
   auto* ptr_last = std::launder(  (T*)(data.data()+highwater-1) );
   auto* ptr_target = std::launder( (T*)(data.data()+i) );
   if (ptr_last!=ptr_target) {
     *ptr_target = std::move(*ptr_last);
   }
   --highwater; // this goes before dtor, in case dtor throws
   ptr_last->~T();
 }