默认情况下,fbl::
中侵入式容器的大多数行为都设计为
在编译时提供尽可能高的安全性,通常通过禁止
使用可能会导致在编译时容易出错的模式。
也就是说,在某些高级场景中,用户可能会选择 绕过这些编译时安全,以故意允许某些 行为无论何时选择使用这些选项 确保您以安全的方式使用该功能
本指南的这一部分将介绍如何执行以下操作:
- 使用
fbl::NodeOptions
选择启用高级行为 - 控制对象在容器中的复制/移动能力。
- 允许
unique_ptr
跟踪的对象包含在多种容器类型中 - 在 O(1) 时间内清除原始指针的容器
- 在不引用容器的情况下从容器中移除对象
使用 fbl::NodeOptions
控制高级选项
为了在编译时控制一些高级选项,节点状态对象
(及其相关的混入)可以采用位标志样式常量,
可用于改变特定行为。您可以使用 |
结合使用多个选项。
运算符。默认情况下,选项是
NodeState
或 Listable
/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_object
到 the_obj
(如果要复制节点状态数据)
yet_another_object
,the_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;
}