高级场景

默认情况下,fbl:: 中侵入式容器的大多数行为都设计为 在编译时提供尽可能高的安全性,通常通过禁止 使用可能会导致在编译时容易出错的模式。

也就是说,在某些高级场景中,用户可能会选择 绕过这些编译时安全,以故意允许某些 行为无论何时选择使用这些选项 确保您以安全的方式使用该功能

本指南的这一部分将介绍如何执行以下操作:

  1. 使用 fbl::NodeOptions 选择启用高级行为
  2. 控制对象在容器中的复制/移动能力。
  3. 允许 unique_ptr 跟踪的对象包含在多种容器类型中
  4. 在 O(1) 时间内清除原始指针的容器
  5. 在不引用容器的情况下从容器中移除对象

使用 fbl::NodeOptions 控制高级选项

为了在编译时控制一些高级选项,节点状态对象 (及其相关的混入)可以采用位标志样式常量, 可用于改变特定行为。您可以使用 | 结合使用多个选项。 运算符。默认情况下,选项是 NodeStateListable/Containable 类型,以及第三个模板参数 TaggedListable/TaggedContainable混音的效果。这些选项始终是默认选项 发送至 fbl::NodeOptions::None。语法如下所示:

class SimpleObject :
  public fbl::DoublyLinkedListable<SimpleObject*, fbl::NodeOption::OptionFoo> { /* ... */ };

class MoreComplexObject :
  public fbl::ContainableBaseClasses<
    fbl::TaggedSinglyLinkedListable<MoreComplex*, Tag1, fbl::NodeOption::OptionBar>,
    fbl::TaggedWAVLTreeContainable <MoreComplex*, Tag2,
                                    fbl::NodeOption::OptionA | fbl::NodeOption::OptionB>> {
  // ...
};

class ExplicitNodesObject {
 public:
  // ...

 private:
  // ...
  static constexpr fbl::NodeOptions kOptions = fbl::NodeOption::OptionX |
                                               fbl::NodeOption::OptionY |
                                               fbl::NodeOption::OptionZ;
  fbl::WAVLTreeNodeState<ExplicitNodesObject*, kOptions> wavl_node_state_;
};

控制对象在容器中的复制/移动行为

当节点的对象位于容器中时,复制或移动节点状态不合法 操作。请注意以下几点:

fbl::DoublyLinkedList<Obj*> the_list;
ASSERT(!the_list.is_empty());

Obj the_obj;
the_list.insert_after(the_list.begin());

Obj another_obj{the_obj};

Obj yet_another_object;
the_obj = yet_another_object;

列表中的第一个节点之后存在 the_obj。如果您要允许该节点 将状态通过默认复制构造函数复制到 another_obj 中,则 有两个对象,其中包含两个簿记副本。another_obj 会 错误地认为它位于容器中,现在将尝试断言 。

更糟糕的是,如果您尝试通过调用 the_list.erase(another_object),您正尝试从 容器处于不连贯状态在这种情况下, 另一个对象指向列表中的第一个对象, 位于示例开头的 begin() 之后,但 *begin() 指向 the_obj,同样, 序列中的下一个对象。虽然具体行为会因类型而异 以及具体的擦除方法 是未定义的行为,无法以完美结尾。

最后,当示例代码将新构建的堆栈分配给 yet_another_objectthe_obj(如果要复制节点状态数据) yet_another_objectthe_obj 甚至会突然认为它不在列表中 尽管它两侧的对象都有指向它的指针

无论你以什么方式看待,允许复制节点状态数据都会损坏数据 是不可避免的,这同样适用于 大多数移动操作的定义。

为了防止出现此类错误,fbl:: 节点状态的默认行为 对象禁止复制构造/赋值以及移动 构建/分配。任何尝试调用复制/移动操作的行为 构造函数/赋值运算符会导致 static_assert 和失败 进行编译

如果对象不在容器中,该怎么办?不应复制/移动 是否允许?简而言之,答案是肯定的,但为了安全起见, 只有在代码作者选择使用相应行为时,才会被视为允许的。 为了选择启用这项功能,您可以将以下 NodeOptions 与其混音功能结合使用 或节点存储类型

  • fbl::NodeOptions::AllowCopy
  • fbl::NodeOptions::AllowMove
  • fbl::NodeOptions::AllowCopyAndMove

设置 AllowCopy 将允许构建和分配复制(左值)数据, 而设置 AllowMove 将允许移动(r 值)构造和 分配。AllowCopyAndMove 是两者的简写形式 总和。

在操作本身期间,节点状态对象将 ZX_DEBUG_ASSERT, 源对象均不在用于构造的容器中,并且 在运行期间,源对象和目标对象均不在容器中。 分配。无论是否启用 ZX_DEBUG_ASERT, 源对象和目标对象的则绝不会修改这些状态。

例如:

struct Point : fbl::DoublyLinkedListable<std::unique_ptr<Point>,
                                         fbl::NodeOptions::AllowCopy> {
  float x, y;
};

fbl::DoublyLinkedList<std::unique_ptr<Point>> all_points;

void AddCopy(const Point& pt) {
  // If pt is in a list, this will assert. If asserts are off, pt will remain
  // where it is and new_pt will not start life in a container.
  auto new_pt = std::make_unique<Point>(pt);
  all_points.push_back(std::move(new_pt));
}

那么,如果您想允许复制或移动 ?例如,如果您想克隆自己的 使用复制构造函数来实现这一目标,该怎么办?用户可以选择启用 以及传递以下各项的适当组合:

  • fbl::NodeOptions::AllowCopyFromContainer
  • fbl::NodeOptions::AllowMoveFromContainer
  • fbl::NodeOptions::AllowCopyAndMoveFromContainer

上述行为保持不变;节点状态永远不会 已更改。新对象将出现在任何容器之外,而源对象将 无论在哪里FromContainer和 此选项与非 FromContainer 版本的区别在于, FromContainer 版本绝不会断言。因此,您可以克隆 并包含以下几项。

struct Point : fbl::DoublyLinkedListable<std::unique_ptr<Point>,
                                         fbl::NodeOptions::AllowCopyFromContainer> {
  float x, y;
};

using PointList = fbl::DoublyLinkedList<std::unique_ptr<Point>>;

PointList CloneList(const PointList& list) {
  PointList ret;
  for (const auto& point : list) {
    ret.push_back(std::make_unique<Point>(point));
  }
  return ret;
}

允许 unique_ptr 跟踪的对象存在于多个容器中

通常,定义一个对象可以存在于 同时跟踪多个容器中的 容器使用 unique_ptr 语义。从理论上讲 让两个不同的容器同时跟踪同一对象 每个查询都使用 unique_ptr 之类的代码,因为这样会违反 指针。

为了防止此处出现任何错误,ContainableBaseClasses不会 允许使用 std::unique_ptr 指针类型作为任何指定的 Mix-ins,除非可包含基类列表的长度正好 1。

struct Tag1 {};
struct Tag2 {};

// This is legal
class Obj : public fbl::ContainableBaseClasses<
  fbl::TaggedSinglyLinkedListable<std::unique_ptr<Obj>, Tag1>> { /* ... */ };

// This is not
class Obj : public fbl::ContainableBaseClasses<
  fbl::TaggedSinglyLinkedListable<std::unique_ptr<Obj>, Tag1>,
  fbl::TaggedSinglyLinkedListable<std::unique_ptr<Obj>, Tag2>> { /* ... */ };

// Neither is this
class Obj : public fbl::ContainableBaseClasses<
  fbl::TaggedSinglyLinkedListable<std::unique_ptr<Obj>, Tag1>,
  fbl::TaggedSinglyLinkedListable<Obj*, Tag2>> { /* ... */ };

不过,多个类型中可能存在多种类型的 这些容器使用 std::unique_ptr 进行管理。

首先,您可能会遇到对象以两种不同的类型存在的情况 数据结构(可能是列表和树),但绝不能使用相同的数据结构 。如果结构的用途完全不相交,您可以 希望放宽此默认限制。

您可能想要允许这样做的第二个原因是,您有一个对象, 使用 std::unique_ptr 的容器跟踪使用寿命,但您希望 允许对象临时存在于容器中,以便获得更多 轻松实施某种算法。也许一组对象需要 过滤为临时列表,然后传递到 过滤后的集合或者,他们可能需要被置于临时 具有自定义排序/键的 WAVLTree,以检查是否存在重复项。

无论出于什么原因,您都可以通过传递 AllowMultiContainerUptr 选项。以下是 不相交容器用例的示例:

struct FreeObjTag {};
struct ActiveObjTag {};
class Obj : public fbl::ContainableBaseClasses<
  fbl::TaggedSinglyLinkedListable<std::unique_ptr<Obj>, FreeObjTag,
                                  fbl::NodeOptions::AllowMultiContainerUptr>,
  fbl::TaggedWAVLTreeContainable<std::unique_ptr<Obj>, ActiveObjTag,
                                 fbl::NodeOptions::AllowMultiContainerUptr>> {
 public:
  using FreeStack = fbl::TaggedSinglyLinkedList<std::unique_ptr<Obj>, FreeObjTag>;
  using ActiveSet = fbl::TaggedWAVLTree<UniqueId, std::unique_ptr<Obj>, ActiveObjTag>;

  // ...
  UniqueId GetKey() const { return unique_id_; }
  void AssignId(UniqueId id) {
    ZX_DEBUG_ASSERT(!fbl::InContainer<ActiveObjTag>(*this));
    unique_id_ = id;
  }
  // ...

 private:
  // ...
};

fbl::Mutex obj_lock_;
Obj::FreeStack free_objects_ TA_GUARDED(obj_lock_);
Obj::ActiveSet active_objects_ TA_GUARDED(obj_lock_);

zx_status_t ActivateObject(UniqueId id) {
  fbl::AutoLock lock(&obj_lock_);

  if (free_objects_.is_empty()) {
    return ZX_ERR_NO_MEMORY;
  }

  auto ptr = free_objects_.pop_front();
  ptr.AssignId(id);
  active_objects_.insert(std::move(ptr));
  return ZX_OK;
}

允许通过 clear_unsafe() 清除 O(1) 容器

生命周期检查部分所述, 如果非托管指针包含的对象且未包含对象,则此类指针不得进行销毁 当认为自己仍在容器中时,无法销毁。两者之一 行为被视为错误,在调试中会触发断言 build。

如果您并不在意对象仍然位于某个位置,该怎么办? 你是否认为自己在毁坏时位于容器里?也许您 分配了一块连续的内存片并将其划分成一个对象, 然后被放入一个免费列表中如果你想释放自己的一块内存 所有对象都已返回到空闲列表, 还是费尽心力?这个 只会白白浪费工作量

您可以通过以下方式绕过这些检查,并跳过对列表的强制性 O(N) 解除关联 使用 AllowClearUnsafe NodeOption使用时,这些声明 会跳过节点状态对象中存在的一个方法, 名为 clear_unsafe() 的应用可供使用。clear_unsafe() 只会 将容器重置为其原始空状态 对象的节点状态。这是一个简单的 O(1) 运算。正在尝试拨打电话 针对使用不带此标志的节点状态对象的容器的 clear_unsafe() 权限 将触发 static_assert。这个简单的示例展示了 如下所示:

class SlabObject :
  public fbl::SinglyLinkedListable<SlabObject*,
                                   fbl::NodeOptions::AllowClearUnsafe> { /* ... */ };

static fbl::Mutex slab_lock;
static SlabObject* slab_memory TA_GUARDED(slab_lock) = nullptr;
static fbl::SizedSinglyLinkedList<SlabObject*> TA_GUARDED(slab_lock) free_objects;

static constexpr size_t kSlabSize = (1 << 20);   // One MB of objects
static constexpr size_t kSlabCount = kSlabSize / sizeof(SlabObject);

zx_status_t InitSlab() {
  fbl::AutoLock lock(&slab_lock);
  if ((slab_memory != nullptr) || !free_objects.is_empty()) {
    return ZX_ERR_BAD_STATE;
  }

  fbl::AllocChecker ac;
  slab_memory = new (&ac) SlabObject[kSlabCount];
  if (!ac.check()) {
    return ZX_ERR_NO_MEMORY;
  }

  for (size_t i = 0; i < kSlabCount; ++i) {
    free_objects.push_front(slab_memory + i);
  }

  return ZX_OK;
}

SlabObject* GetFreeObj() {
  fbl::AutoLock lock(&slab_lock);
  return !free_objects.is_empty() ? free_objects.pop_front() : nullptr;
}

void ReturnObj(SlabObject* obj) {
  fbl::AutoLock lock(&slab_lock);
  ZX_DEBUG_ASSERT(obj != nullptr);
  free_objects.push_front(obj);
}

zx_status_t DeinitSlab() {
  fbl::AutoLock lock(&slab_lock);

  // If not all of our objects have returned, or if we don't have any slab
  // memory allocated, then we cannot de-init our slab.
  if ((slab_memory == nullptr) || (free_objects.size() != kSlabCount)) {
    return ZX_ERR_BAD_STATE;
  }

  // Great, reset the free list with clear unsafe. This basically just sets the
  // head pointer to nullptr.
  free_objects.clear_unsafe();

  // Now give our memory back. Since our objects are flagged with
  // AllowClearUnsafe, node state destructors do nothing. Provided that
  // SlabObject destructors do nothing, this delete should just return memory to
  // the heap and not need to call N destructors.
  delete[] free_objects;
  free_objects = nullptr;

  return ZX_OK;
}

直接从对象所在的任何容器实例中移除对象。

一般来说,虽然有时可行,但并不是 设计需要直接从容器中移除对象的代码 而无需引用容器本身作为一项设计原则, 入侵性容器时,应该始终清楚哪些容器类型和 始终存在对象。尽管如此,有时直接移除 是最简便、最佳的选择。

默认情况下,跟踪大小的容器可能需要 数据结构,以便找到容器,以便在有节点时更新簿记 在不知道容器实例的情况下从容器中移除。 因此,这些容器不支持直接移除。其他容器 (例如 SinglyLinkedList), 返回指针指向其上一个节点。

然而,规模不大的双链接列表可以支持直接 节点移除。如需启用此功能,请将 AllowRemoveFromContainer 添加到节点中 状态的 NodeOption。启用后,节点状态结构将具有 RemoveFromContainer() 方法可用。调用 RemoveFromContainer 现为 与调用 InContainer 完全相同。如果存在以下情况,则可以直接从对象调用该方法: 明确类型来选择要移除的容器 从继承产生歧义时开始, fbl::RemoveFromContaier<Tag>(obj_ref) 调用(使用 ContainableBaseClasses 帮助程序。请参阅 InContainer()

请参考以下用例。您有很多工作需要 由流水线的多个阶段处理。每个流水线阶段都有一个 待处理工作队列,线程从该队列获取作业,然后处理作业,然后将作业加入队列 进入流水线的下一阶段。

如果您想在执行作业时取消作业,如何轻松了解是 处于哪个渠道阶段?有一个答案可能是,只要 您可以直接将其从当前所在的处理阶段中移除。这个 最终可能如下所示:

struct PipelineTag {};
struct ActiveTag {};

fbl::Mutex pipeline_lock;

class Job : public fbl::RefCounted<Job>,
            public fbl::ContainableBaseClasses<
              fbl::TaggedDoublyLinkedListable<fbl::RefPtr<Job>, PipelineTag,
                                              fbl::NodeOptions::AllowRemoveFromContainer>,
              fbl::TaggedWAVLTreeContainable<fbl::RefPtr<Job>, ActiveTag>> {
 public:
  // ...
  UniqueId GetKey() const { return unique_id_; }
  bool is_canceled() const TA_REQ(pipeline_lock) { return cancel_flag_; }
  void set_canceled() TA_REQ(pipeline_lock) { cancel_flag_ = true; }
  // ...
 private:
  bool cancel_flag_ TA_GUARDED(pipeline_lock) = false;
};

using PipelineQueue = fbl::TaggedDoublyLinkedList<fbl::RefPtr<Job>, PipelineTag>;
std::array<PipelineQueue, 10> pipeline_stages TA_GUARDED(pipeline_lock);
fbl::TaggedWAVLTree<fbl::RefPtr<Job>, ActiveTag> active_jobs TA_GUARDED(pipeline_lock);

zx_status_t QueueJob(fbl::RefPtr<Job> job) {
  ZX_DEBUG_ASSERT(job != nullptr);
  {
    fbl::AutoLock lock(&pipeline_lock);

    // Can't queue a job for processing if it is already being processed.
    if (fbl::InContainer<ActiveTag>(*job)) {
      return ZX_ERR_BAD_STATE;
    }

    // If we are not in the active set, then we had better not be in any of the
    // pipeline stages.
    ZX_DEBUG_ASSERT(!fbl::InContainer<PipelineTag>(*job));

    // Put the job into the active set and into the first pipeline stage.
    active_jobs.insert(job);
    pipeline_stages[0].push_back(std::move(job));
  }

  SignalPipelineStage(0);
}

void WorkThread(size_t pipeline_stage) {
  ZX_DEBUG_ASSERT(pipeline_stage < pipeline_stages.size());
  PipelineQueue& our_stage = pipeline_stages[pipeline_stage];
  PipelineQueue* next_stage = ((pipeline_stage + 1) < pipeline_stages.size())
                            ? (pipeline_stages + pipeline_stage + 1)
                            : nullptr;

  while (!QuitTime()) {
    fbl::RefPtr<Job> job;
    {
      // If there is work in our stage, take it out and get to work.
      fbl::AutoLock lock(&pipeline_lock);
      if (!our_stage.is_empty()) {
        job = our_stage.pop_front();
      }
    }

    // Did we not find a job? Just wait for something to do then.
    if (job == nullptr) {
      WaitForPipelineStageWorkOrQuit(pipeline_stage);
      continue;
    }

    // Do the job.
    ProcessJob(job, pipeline_stage);

    // If the job was canceled or reached the end of the pipeline, we will call
    // a handler to take care of it once we are out of the lock.
    void(*handler)(fbl::RefPtr<Job>) = nullptr;
    {
      fbl::AutoLock lock(&pipeline_lock);

      if (job->is_canceled()) {
        // Handle job cancellation if it was flagged for cancel while we were
        // working. No need to take it out of the active set, the cancel
        // operation should have already done that for us.
        ZX_DEBUG_ASSERT(!fbl::InContainer<ActiveTag>(*job));
        handler = HandleCanceledJob;
      } else if (next_stage != nullptr) {
        // Queue to the next stage if there is one.
        next_stage->push_back(std::move(job));
        signal_next_stage = true;
      } else {
        // End of pipeline. This job is finished, remember to take it out of
        // the active set.
        ZX_DEBUG_ASSERT(fbl::InContainer<ActiveTag>(*job));
        active_jobs.erase(*job);
        handler = HandleFinishedJob;
      }
    }

    // Now that we are out of the lock, either signal the next stage so that it
    // knows that it might have some work, or call the chosen handler on the job.
    if (handler) {
      ZX_DEBUG_ASERT(job != nullptr);
      handler(std::move(job));
    } else {
      SignalPipelineStage(pipeline_stage + 1);
    }
  }
}

zx_status_t CancelJob(UniqueId id) {
  fbl::RefPtr<Job> canceled_job;
  {
    fbl::AutoLock lock(&pipeline_lock);

    // Is there an active job with the provided ID?
    auto iter = active_jobs.find(id);
    if (!iter.IsValid()) {
      return ZX_ERR_NOT_FOUND;
    }

    // No matter what, the job is no longer active. Take its reference back from
    // the active job set.
    fbl::RefPtr<Job> job = active_jobs.erase(iter);

    // Flag the job as canceled.
    job->set_canceled();

    // If the job is in a pipeline stage, then no thread is currently working on
    // it. We can just pull it out of whatever stage we are in and we are done.
    if (fbl::InContainer<PipelineTag>(*job)) {
      canceled_job = fbl::RemoveFromContainer<PipelineTag>(*job);
    }
  }

  // Now that we are out of the lock, if we were the ones to pull the job out of
  // the pipeline, we should hand it over to the cancel handler.
  HandleCanceledJob(std::move(canceled_job));
  return ZX_OK;
}